Skip to content

Move SnapshotParcels.kt to workflow-core. #1373

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions artifacts.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@
"javaVersion": 8,
"publicationName": "maven"
},
{
"gradlePath": ":workflow-core",
"group": "com.squareup.workflow1",
"artifactId": "workflow-core-android",
"description": "Workflow Core",
"packaging": "aar",
"javaVersion": 8,
"publicationName": "android"
},
{
"gradlePath": ":workflow-core",
"group": "com.squareup.workflow1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.WorkflowAction.Companion.noAction
import com.squareup.workflow1.action
import com.squareup.workflow1.toParcelable
import com.squareup.workflow1.toSnapshot
import com.squareup.workflow1.ui.AndroidScreen
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ScreenViewFactory
Expand All @@ -22,8 +24,6 @@ import com.squareup.workflow1.ui.navigation.AlertOverlay.Event.ButtonClicked
import com.squareup.workflow1.ui.navigation.AlertOverlay.Event.Canceled
import com.squareup.workflow1.ui.navigation.BackButtonScreen
import com.squareup.workflow1.ui.navigation.BodyAndOverlaysScreen
import com.squareup.workflow1.ui.toParcelable
import com.squareup.workflow1.ui.toSnapshot
import kotlinx.parcelize.Parcelize

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import com.squareup.sample.hellobackbutton.HelloBackButtonWorkflow.State.Baker
import com.squareup.sample.hellobackbutton.HelloBackButtonWorkflow.State.Charlie
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.ui.toParcelable
import com.squareup.workflow1.ui.toSnapshot
import com.squareup.workflow1.toParcelable
import com.squareup.workflow1.toSnapshot
import kotlinx.parcelize.Parcelize

object HelloBackButtonWorkflow : StatefulWorkflow<Unit, State, Nothing, HelloBackButtonScreen>() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ public final class com/squareup/workflow1/WorkflowIdentifier$Companion {
public final fun parse (Lokio/ByteString;)Lcom/squareup/workflow1/WorkflowIdentifier;
}

public final class com/squareup/workflow1/WorkflowIdentifierExKt {
public final class com/squareup/workflow1/WorkflowIdentifierEx_jvmKt {
public static final fun getWorkflowIdentifier (Lkotlin/reflect/KClass;)Lcom/squareup/workflow1/WorkflowIdentifier;
}

Expand Down
64 changes: 55 additions & 9 deletions workflow-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import com.squareup.workflow1.buildsrc.iosWithSimulatorArm64

plugins {
id("kotlin-multiplatform")
id("com.android.kotlin.multiplatform.library")
id("published")
}

Expand All @@ -13,18 +14,63 @@ kotlin {
if (targets == "kmp" || targets == "jvm") {
jvm { withJava() }
}
// The default KMP
// ["hierarchy template"](https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-hierarchy.html#see-the-full-hierarchy-template)
// configures `androidMain` and `jvmMain` to be entirely separate targets, even though Android
// *can* be made to be a child of JVM. Changing this requires completely wiring up all targets
// ourselves though, so for now we've left them separate to simplify gradle config. If there ends
// up being too much code duplication, we can either make `androidMain` a child of `jvmMain`, or
// introduce a new shared target that includes both of them. Compose, for example, uses a
// structure where `jvm` is the shared parent of both `android` and `desktop`.
if (targets == "kmp" || targets == "android") {
androidLibrary {
namespace = "com.squareup.workflow1.android"
testNamespace = "$namespace.test"

compileSdk = libs.versions.compileSdk.get().toInt()
minSdk = libs.versions.minSdk.get().toInt()

withHostTestBuilder {
}.configure {
}

withDeviceTestBuilder {
sourceSetTreeName = "test"
}.configure {
instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

// Disable transition and rotation animations.
animationsDisabled = true
}
}
}
if (targets == "kmp" || targets == "js") {
js(IR) { browser() }
}
}

dependencies {
commonMainApi(libs.kotlin.jdk6)
commonMainApi(libs.kotlinx.coroutines.core)
// For Snapshot.
commonMainApi(libs.squareup.okio)
sourceSets {
commonMain {
dependencies {
api(libs.kotlin.jdk6)
api(libs.kotlinx.coroutines.core)
// For Snapshot.
api(libs.squareup.okio)
}
}

commonTestImplementation(libs.kotlinx.atomicfu)
commonTestImplementation(libs.kotlinx.coroutines.test.common)
commonTestImplementation(libs.kotlin.test.core)
commonTest {
dependencies {
implementation(libs.kotlinx.atomicfu)
implementation(libs.kotlinx.coroutines.test.common)
implementation(libs.kotlin.test.core)
}
}

getByName("androidHostTest") {
dependencies {
implementation(libs.robolectric)
implementation(libs.robolectric.annotations)
}
}
}
}
11 changes: 11 additions & 0 deletions workflow-core/dependencies/androidRuntimeClasspath.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
com.squareup.okio:okio-jvm:3.3.0
com.squareup.okio:okio:3.3.0
org.jetbrains.kotlin:kotlin-bom:2.0.21
org.jetbrains.kotlin:kotlin-stdlib-common:2.0.21
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.0.21
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21
org.jetbrains.kotlin:kotlin-stdlib:2.0.21
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3
org.jetbrains:annotations:23.0.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.squareup.workflow1

import android.os.Bundle
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

@RunWith(RobolectricTestRunner::class)
class SnapshotParcelsTest {

@Test fun parcelableToSnapshot_savesAndRestores() {
val snapshot = Bundle().apply {
putString("key", "value")
}.toSnapshot()
val restored = snapshot.toParcelable<Bundle>()

assertNotNull(restored)
assertTrue(restored.containsKey("key"))
assertEquals("value", restored.getString("key"))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.squareup.workflow1

import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Parcel
import android.os.Parcelable
import okio.ByteString

/**
* Wraps receiver in a [Snapshot] suitable for use with [StatefulWorkflow].
* Intended to allow use of `@Parcelize`.
*
* Read the [Parcelable] back with [toParcelable].
*/
public fun Parcelable.toSnapshot(): Snapshot = Snapshot.write { bufferedSink ->
val parcel = Parcel.obtain()
parcel.writeParcelable(this, 0)
val byteArray = parcel.marshall()
bufferedSink.write(byteArray)
parcel.recycle()
}

/**
* Returns a [Parcelable] previously wrapped with [toSnapshot], or `null` if the receiver is empty.
*/
public inline fun <reified T : Parcelable> Snapshot.toParcelable(): T? =
bytes.toParcelable<T>()

public inline fun <reified T : Parcelable> ByteString.toParcelable(): T? =
toParcelable(T::class.java)

@PublishedApi
internal fun <T : Parcelable> ByteString.toParcelable(targetClass: Class<T>): T? {
if (size == 0) return null

val parcel = Parcel.obtain()
val byteArray = toByteArray()
parcel.unmarshall(byteArray, 0, byteArray.size)
parcel.setDataPosition(0)
val rtn = if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
parcel.readParcelable(Snapshot::class.java.classLoader, targetClass)!!
} else {
@Suppress("DEPRECATION")
parcel.readParcelable(Snapshot::class.java.classLoader)!!
}
parcel.recycle()
return rtn
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.squareup.workflow1

import kotlin.reflect.KClass

internal actual fun commonUniqueClassName(kClass: KClass<*>): String {
return kClass.qualifiedName ?: kClass.toString()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.squareup.workflow1

import com.squareup.workflow1.WorkflowIdentifierType.Snapshottable
import org.jetbrains.annotations.TestOnly
import kotlin.reflect.KClass

/**
* The [WorkflowIdentifier] that identifies the workflow this [KClass] represents.
*
* This workflow must not be an [ImpostorWorkflow], or this property will throw an
* [IllegalArgumentException].
*/
@get:TestOnly
public val KClass<out Workflow<*, *, *>>.workflowIdentifier: WorkflowIdentifier
get() {
val workflowClass = this@workflowIdentifier
require(!ImpostorWorkflow::class.java.isAssignableFrom(workflowClass.java)) {
"Cannot create WorkflowIdentifier from a KClass of ImpostorWorkflow: " +
workflowClass.qualifiedName.toString()
}
return WorkflowIdentifier(type = Snapshottable(workflowClass))
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.squareup.workflow1.ui

import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Parcel
import android.os.Parcelable
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.toParcelable
import com.squareup.workflow1.toSnapshot
import okio.ByteString

/**
Expand All @@ -13,35 +12,23 @@ import okio.ByteString
*
* Read the [Parcelable] back with [toParcelable].
*/
public fun Parcelable.toSnapshot(): Snapshot {
return Snapshot.write { bufferedSink ->
val parcel = Parcel.obtain()
parcel.writeParcelable(this, 0)
val byteArray = parcel.marshall()
bufferedSink.write(byteArray)
parcel.recycle()
}
}
@Deprecated(
"Use toSnapshot() from workflow-core instead.",
replaceWith = ReplaceWith("toSnapshot()", "com.squareup.workflow1.toSnapshot")
)
public fun Parcelable.toSnapshot(): Snapshot = toSnapshot()

/**
* @return a [Parcelable] previously wrapped with [toSnapshot], or `null` if the receiver is empty.
*/
public inline fun <reified T : Parcelable> Snapshot.toParcelable(): T? {
return bytes.takeIf { it.size > 0 }
?.toParcelable<T>()
}
@Deprecated(
"Use toParcelable() from workflow-core instead.",
replaceWith = ReplaceWith("toParcelable()", "com.squareup.workflow1.toParcelable")
)
public inline fun <reified T : Parcelable> Snapshot.toParcelable(): T? = toParcelable()

public inline fun <reified T : Parcelable> ByteString.toParcelable(): T {
val parcel = Parcel.obtain()
val byteArray = toByteArray()
parcel.unmarshall(byteArray, 0, byteArray.size)
parcel.setDataPosition(0)
val rtn = if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
parcel.readParcelable<T>(Snapshot::class.java.classLoader, T::class.java)!!
} else {
@Suppress("DEPRECATION")
parcel.readParcelable<T>(Snapshot::class.java.classLoader)!!
}
parcel.recycle()
return rtn
}
@Deprecated(
"Use toParcelable() from workflow-core instead.",
replaceWith = ReplaceWith("toParcelable()", "com.squareup.workflow1.toParcelable")
)
public inline fun <reified T : Parcelable> ByteString.toParcelable(): T = toParcelable<T>()!!
Loading