From c1df8c5fcb2561d90260f1ae430608f632eb13b7 Mon Sep 17 00:00:00 2001 From: Zach Klippenstein Date: Thu, 17 Jul 2025 08:25:23 -0700 Subject: [PATCH 1/2] Move SnapshotParcels.kt to workflow-core. This gives workflow-core an android source set and moves the Parcelable-related Snapshot helpers to it. This makes more sense since the other Snapshot helpers are in the core module, and Parcelable isn't a UI thing it's just an android thing. This also paves the way for go/compose-based-workflow to use these helpers to support `rememberSaveable`. --- .../hellobackbutton/AreYouSureWorkflow.kt | 4 +- .../HelloBackButtonWorkflow.kt | 4 +- workflow-core/api/{ => jvm}/workflow-core.api | 2 +- workflow-core/build.gradle.kts | 64 ++++++++++++++++--- .../dependencies/androidRuntimeClasspath.txt | 11 ++++ .../squareup/workflow1/SnapshotParcelsTest.kt | 24 +++++++ .../CommonUniqueClassName.android.kt} | 0 .../com/squareup/workflow1/SnapshotParcels.kt | 48 ++++++++++++++ .../WorkflowIdentifierEx.android.kt} | 0 .../workflow1/CommonUniqueClassName.jvm.kt | 7 ++ .../workflow1/WorkflowIdentifierEx.jvm.kt | 22 +++++++ .../squareup/workflow1/ui/SnapshotParcels.kt | 47 +++++--------- 12 files changed, 189 insertions(+), 44 deletions(-) rename workflow-core/api/{ => jvm}/workflow-core.api (99%) create mode 100644 workflow-core/dependencies/androidRuntimeClasspath.txt create mode 100644 workflow-core/src/androidHostTest/kotlin/com/squareup/workflow1/SnapshotParcelsTest.kt rename workflow-core/src/{jvmMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.kt => androidMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.android.kt} (100%) create mode 100644 workflow-core/src/androidMain/kotlin/com/squareup/workflow1/SnapshotParcels.kt rename workflow-core/src/{jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.kt => androidMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.android.kt} (100%) create mode 100644 workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.jvm.kt create mode 100644 workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.jvm.kt diff --git a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt index 98eac1d4e8..be5541b808 100644 --- a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt +++ b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt @@ -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 @@ -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 /** diff --git a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonWorkflow.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonWorkflow.kt index 01c0960f02..296a522354 100644 --- a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonWorkflow.kt +++ b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonWorkflow.kt @@ -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() { diff --git a/workflow-core/api/workflow-core.api b/workflow-core/api/jvm/workflow-core.api similarity index 99% rename from workflow-core/api/workflow-core.api rename to workflow-core/api/jvm/workflow-core.api index 0d4ec7f2cd..76730f4cda 100644 --- a/workflow-core/api/workflow-core.api +++ b/workflow-core/api/jvm/workflow-core.api @@ -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; } diff --git a/workflow-core/build.gradle.kts b/workflow-core/build.gradle.kts index c3c841f414..06dff72422 100644 --- a/workflow-core/build.gradle.kts +++ b/workflow-core/build.gradle.kts @@ -2,6 +2,7 @@ import com.squareup.workflow1.buildsrc.iosWithSimulatorArm64 plugins { id("kotlin-multiplatform") + id("com.android.kotlin.multiplatform.library") id("published") } @@ -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) + } + } + } } diff --git a/workflow-core/dependencies/androidRuntimeClasspath.txt b/workflow-core/dependencies/androidRuntimeClasspath.txt new file mode 100644 index 0000000000..1782411506 --- /dev/null +++ b/workflow-core/dependencies/androidRuntimeClasspath.txt @@ -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 diff --git a/workflow-core/src/androidHostTest/kotlin/com/squareup/workflow1/SnapshotParcelsTest.kt b/workflow-core/src/androidHostTest/kotlin/com/squareup/workflow1/SnapshotParcelsTest.kt new file mode 100644 index 0000000000..b1d29b25bf --- /dev/null +++ b/workflow-core/src/androidHostTest/kotlin/com/squareup/workflow1/SnapshotParcelsTest.kt @@ -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() + + assertNotNull(restored) + assertTrue(restored.containsKey("key")) + assertEquals("value", restored.getString("key")) + } +} diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.kt b/workflow-core/src/androidMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.android.kt similarity index 100% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.kt rename to workflow-core/src/androidMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.android.kt diff --git a/workflow-core/src/androidMain/kotlin/com/squareup/workflow1/SnapshotParcels.kt b/workflow-core/src/androidMain/kotlin/com/squareup/workflow1/SnapshotParcels.kt new file mode 100644 index 0000000000..49ad7aacba --- /dev/null +++ b/workflow-core/src/androidMain/kotlin/com/squareup/workflow1/SnapshotParcels.kt @@ -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 Snapshot.toParcelable(): T? = + bytes.toParcelable() + +public inline fun ByteString.toParcelable(): T? = + toParcelable(T::class.java) + +@PublishedApi +internal fun ByteString.toParcelable(targetClass: Class): 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 +} diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.kt b/workflow-core/src/androidMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.android.kt similarity index 100% rename from workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.kt rename to workflow-core/src/androidMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.android.kt diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.jvm.kt b/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.jvm.kt new file mode 100644 index 0000000000..96e01ede7a --- /dev/null +++ b/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.jvm.kt @@ -0,0 +1,7 @@ +package com.squareup.workflow1 + +import kotlin.reflect.KClass + +internal actual fun commonUniqueClassName(kClass: KClass<*>): String { + return kClass.qualifiedName ?: kClass.toString() +} diff --git a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.jvm.kt b/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.jvm.kt new file mode 100644 index 0000000000..494578db58 --- /dev/null +++ b/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.jvm.kt @@ -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>.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)) + } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/SnapshotParcels.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/SnapshotParcels.kt index 4ee8c37f7c..2d400d0a8c 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/SnapshotParcels.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/SnapshotParcels.kt @@ -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 /** @@ -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 Snapshot.toParcelable(): T? { - return bytes.takeIf { it.size > 0 } - ?.toParcelable() -} +@Deprecated( + "Use toParcelable() from workflow-core instead.", + replaceWith = ReplaceWith("toParcelable()", "com.squareup.workflow1.toParcelable") +) +public inline fun Snapshot.toParcelable(): T? = toParcelable() -public inline fun 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(Snapshot::class.java.classLoader, T::class.java)!! - } else { - @Suppress("DEPRECATION") - parcel.readParcelable(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 ByteString.toParcelable(): T = toParcelable()!! From d0ac74697db5ccf1cd571bf7d39bd8d56adc0404 Mon Sep 17 00:00:00 2001 From: zach-klippenstein <101754+zach-klippenstein@users.noreply.github.com> Date: Thu, 17 Jul 2025 15:55:16 +0000 Subject: [PATCH 2/2] Apply changes from artifactsDump Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- artifacts.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/artifacts.json b/artifacts.json index 2cfb78b7bc..38f1d3b3b8 100644 --- a/artifacts.json +++ b/artifacts.json @@ -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",