diff --git a/artifacts.json b/artifacts.json index 38f1d3b3b8..21b078b895 100644 --- a/artifacts.json +++ b/artifacts.json @@ -17,15 +17,6 @@ "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", @@ -80,15 +71,6 @@ "javaVersion": 8, "publicationName": "kotlinMultiplatform" }, - { - "gradlePath": ":workflow-runtime", - "group": "com.squareup.workflow1", - "artifactId": "workflow-runtime-android", - "description": "Workflow Runtime", - "packaging": "aar", - "javaVersion": 8, - "publicationName": "android" - }, { "gradlePath": ":workflow-runtime", "group": "com.squareup.workflow1", @@ -143,6 +125,15 @@ "javaVersion": 8, "publicationName": "kotlinMultiplatform" }, + { + "gradlePath": ":workflow-runtime-android", + "group": "com.squareup.workflow1", + "artifactId": "workflow-runtime-android", + "description": "Workflow Runtime Android", + "packaging": "aar", + "javaVersion": 8, + "publicationName": "maven" + }, { "gradlePath": ":workflow-rx2", "group": "com.squareup.workflow1", 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 be5541b808..98eac1d4e8 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,8 +10,6 @@ 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 @@ -24,6 +22,8 @@ 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 296a522354..01c0960f02 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.toParcelable -import com.squareup.workflow1.toSnapshot +import com.squareup.workflow1.ui.toParcelable +import com.squareup.workflow1.ui.toSnapshot import kotlinx.parcelize.Parcelize object HelloBackButtonWorkflow : StatefulWorkflow() { diff --git a/settings.gradle.kts b/settings.gradle.kts index f4982b30f7..00f951b069 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -64,6 +64,7 @@ include( ":workflow-config:config-jvm", ":workflow-core", ":workflow-runtime", + ":workflow-runtime-android", ":workflow-rx2", ":workflow-testing", ":workflow-tracing", diff --git a/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt b/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt index 20e8bd0fa9..1782411506 100644 --- a/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt @@ -1,58 +1,3 @@ -androidx.activity:activity-ktx:1.7.0 -androidx.activity:activity:1.7.0 -androidx.annotation:annotation-experimental:1.4.1 -androidx.annotation:annotation-jvm:1.8.1 -androidx.annotation:annotation:1.8.1 -androidx.arch.core:core-common:2.2.0 -androidx.arch.core:core-runtime:2.2.0 -androidx.autofill:autofill:1.0.0 -androidx.collection:collection-jvm:1.4.4 -androidx.collection:collection-ktx:1.4.4 -androidx.collection:collection:1.4.4 -androidx.compose.runtime:runtime-android:1.7.2 -androidx.compose.runtime:runtime-saveable-android:1.7.2 -androidx.compose.runtime:runtime-saveable:1.7.2 -androidx.compose.runtime:runtime:1.7.2 -androidx.compose.ui:ui-android:1.7.2 -androidx.compose.ui:ui-geometry-android:1.7.2 -androidx.compose.ui:ui-geometry:1.7.2 -androidx.compose.ui:ui-graphics-android:1.7.2 -androidx.compose.ui:ui-graphics:1.7.2 -androidx.compose.ui:ui-text-android:1.7.2 -androidx.compose.ui:ui-text:1.7.2 -androidx.compose.ui:ui-unit-android:1.7.2 -androidx.compose.ui:ui-unit:1.7.2 -androidx.compose.ui:ui-util-android:1.7.2 -androidx.compose.ui:ui-util:1.7.2 -androidx.compose:compose-bom:2024.09.02 -androidx.concurrent:concurrent-futures:1.1.0 -androidx.core:core-ktx:1.12.0 -androidx.core:core:1.12.0 -androidx.customview:customview-poolingcontainer:1.0.0 -androidx.emoji2:emoji2:1.2.0 -androidx.graphics:graphics-path:1.0.1 -androidx.interpolator:interpolator:1.0.0 -androidx.lifecycle:lifecycle-common-jvm:2.8.7 -androidx.lifecycle:lifecycle-common:2.8.7 -androidx.lifecycle:lifecycle-livedata-core:2.8.7 -androidx.lifecycle:lifecycle-process:2.8.7 -androidx.lifecycle:lifecycle-runtime-android:2.8.7 -androidx.lifecycle:lifecycle-runtime-compose-android:2.8.7 -androidx.lifecycle:lifecycle-runtime-compose:2.8.7 -androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.7 -androidx.lifecycle:lifecycle-runtime-ktx:2.8.7 -androidx.lifecycle:lifecycle-runtime:2.8.7 -androidx.lifecycle:lifecycle-viewmodel-android:2.8.7 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7 -androidx.lifecycle:lifecycle-viewmodel:2.8.7 -androidx.profileinstaller:profileinstaller:1.3.1 -androidx.savedstate:savedstate-ktx:1.2.1 -androidx.savedstate:savedstate:1.2.1 -androidx.startup:startup-runtime:1.1.1 -androidx.tracing:tracing:1.0.0 -androidx.versionedparcelable:versionedparcelable:1.1.1 -com.google.guava:listenablefuture:1.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 org.jetbrains.kotlin:kotlin-bom:2.0.21 @@ -60,7 +5,6 @@ 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-android:1.7.3 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 diff --git a/workflow-core/api/jvm/workflow-core.api b/workflow-core/api/workflow-core.api similarity index 99% rename from workflow-core/api/jvm/workflow-core.api rename to workflow-core/api/workflow-core.api index 76730f4cda..0d4ec7f2cd 100644 --- a/workflow-core/api/jvm/workflow-core.api +++ b/workflow-core/api/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/WorkflowIdentifierEx_jvmKt { +public final class com/squareup/workflow1/WorkflowIdentifierExKt { 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 06dff72422..c3c841f414 100644 --- a/workflow-core/build.gradle.kts +++ b/workflow-core/build.gradle.kts @@ -2,7 +2,6 @@ import com.squareup.workflow1.buildsrc.iosWithSimulatorArm64 plugins { id("kotlin-multiplatform") - id("com.android.kotlin.multiplatform.library") id("published") } @@ -14,63 +13,18 @@ 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() } } +} - sourceSets { - commonMain { - dependencies { - api(libs.kotlin.jdk6) - api(libs.kotlinx.coroutines.core) - // For Snapshot. - api(libs.squareup.okio) - } - } - - commonTest { - dependencies { - implementation(libs.kotlinx.atomicfu) - implementation(libs.kotlinx.coroutines.test.common) - implementation(libs.kotlin.test.core) - } - } +dependencies { + commonMainApi(libs.kotlin.jdk6) + commonMainApi(libs.kotlinx.coroutines.core) + // For Snapshot. + commonMainApi(libs.squareup.okio) - getByName("androidHostTest") { - dependencies { - implementation(libs.robolectric) - implementation(libs.robolectric.annotations) - } - } - } + commonTestImplementation(libs.kotlinx.atomicfu) + commonTestImplementation(libs.kotlinx.coroutines.test.common) + commonTestImplementation(libs.kotlin.test.core) } diff --git a/workflow-core/dependencies/androidRuntimeClasspath.txt b/workflow-core/dependencies/androidRuntimeClasspath.txt deleted file mode 100644 index 1782411506..0000000000 --- a/workflow-core/dependencies/androidRuntimeClasspath.txt +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index b1d29b25bf..0000000000 --- a/workflow-core/src/androidHostTest/kotlin/com/squareup/workflow1/SnapshotParcelsTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -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/androidMain/kotlin/com/squareup/workflow1/SnapshotParcels.kt b/workflow-core/src/androidMain/kotlin/com/squareup/workflow1/SnapshotParcels.kt deleted file mode 100644 index 49ad7aacba..0000000000 --- a/workflow-core/src/androidMain/kotlin/com/squareup/workflow1/SnapshotParcels.kt +++ /dev/null @@ -1,48 +0,0 @@ -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/CommonUniqueClassName.jvm.kt b/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.jvm.kt deleted file mode 100644 index 96e01ede7a..0000000000 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.jvm.kt +++ /dev/null @@ -1,7 +0,0 @@ -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/androidMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.android.kt b/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.kt similarity index 100% rename from workflow-core/src/androidMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.android.kt rename to workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/CommonUniqueClassName.kt 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 deleted file mode 100644 index 494578db58..0000000000 --- a/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.jvm.kt +++ /dev/null @@ -1,22 +0,0 @@ -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-core/src/androidMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.android.kt b/workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.kt similarity index 100% rename from workflow-core/src/androidMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.android.kt rename to workflow-core/src/jvmMain/kotlin/com/squareup/workflow1/WorkflowIdentifierEx.kt diff --git a/workflow-runtime-android/README.md b/workflow-runtime-android/README.md new file mode 100644 index 0000000000..f6ede30c25 --- /dev/null +++ b/workflow-runtime-android/README.md @@ -0,0 +1,11 @@ +# Module Workflow Runtime Android + +This module is an Android library that contains utilities to start a Workflow runtime that are +specific to Android components. This contains only the 'headless' components for Workflow on +Android; i.e. no UI concerns. + +See :workflow-ui:core-android for the complimentary helpers on Android that include UI concerns: +view model persistent, `WorkflowLayout`, etc. + +It also provides a place to include tests that verify behaviour of the runtime while using +Android specific dispatchers. diff --git a/workflow-runtime-android/api/workflow-runtime-android.api b/workflow-runtime-android/api/workflow-runtime-android.api new file mode 100644 index 0000000000..4316d5d143 --- /dev/null +++ b/workflow-runtime-android/api/workflow-runtime-android.api @@ -0,0 +1,10 @@ +public final class com/squareup/workflow1/android/AndroidRenderWorkflowKt { + public static final fun removeWorkflowState (Landroidx/lifecycle/SavedStateHandle;)V + public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; + public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; + public static final fun renderWorkflowIn (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; + public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; + public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; + public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow1/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Landroidx/lifecycle/SavedStateHandle;Ljava/util/List;Ljava/util/Set;Lcom/squareup/workflow1/WorkflowTracer;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; +} + diff --git a/workflow-runtime-android/build.gradle.kts b/workflow-runtime-android/build.gradle.kts new file mode 100644 index 0000000000..1dfc2c7b36 --- /dev/null +++ b/workflow-runtime-android/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("android-defaults") + id("android-ui-tests") + id("app.cash.burst") + id("published") +} + +android { + namespace = "com.squareup.workflow1.android" + testNamespace = "$namespace.test" +} + +dependencies { + val composeBom = platform(libs.androidx.compose.bom) + + api(project(":workflow-runtime")) + api(libs.androidx.compose.ui.android) + api(libs.androidx.lifecycle.viewmodel.savedstate) + + implementation(composeBom) + implementation(project(":workflow-core")) + + androidTestImplementation(libs.androidx.activity.ktx) + androidTestImplementation(libs.androidx.lifecycle.viewmodel.ktx) + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.truth) + androidTestImplementation(libs.kotlin.test.core) + androidTestImplementation(libs.kotlin.test.jdk) + androidTestImplementation(libs.kotlinx.coroutines.android) + androidTestImplementation(libs.kotlinx.coroutines.core) + androidTestImplementation(libs.kotlinx.coroutines.test) + androidTestImplementation(libs.squareup.papa) +} diff --git a/workflow-runtime/dependencies/androidRuntimeClasspath.txt b/workflow-runtime-android/dependencies/releaseRuntimeClasspath.txt similarity index 100% rename from workflow-runtime/dependencies/androidRuntimeClasspath.txt rename to workflow-runtime-android/dependencies/releaseRuntimeClasspath.txt diff --git a/workflow-runtime-android/gradle.properties b/workflow-runtime-android/gradle.properties new file mode 100644 index 0000000000..5f09c5c151 --- /dev/null +++ b/workflow-runtime-android/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=workflow-runtime-android +POM_NAME=Workflow Runtime Android +POM_PACKAGING=aar diff --git a/workflow-runtime/src/androidDeviceTest/kotlin/AndroidManifest.xml b/workflow-runtime-android/src/androidTest/AndroidManifest.xml similarity index 100% rename from workflow-runtime/src/androidDeviceTest/kotlin/AndroidManifest.xml rename to workflow-runtime-android/src/androidTest/AndroidManifest.xml diff --git a/workflow-runtime/src/androidDeviceTest/kotlin/com/squareup/workflow1/android/AndroidDispatchersRenderWorkflowInTest.kt b/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidDispatchersRenderWorkflowInTest.kt similarity index 100% rename from workflow-runtime/src/androidDeviceTest/kotlin/com/squareup/workflow1/android/AndroidDispatchersRenderWorkflowInTest.kt rename to workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidDispatchersRenderWorkflowInTest.kt diff --git a/workflow-runtime/src/androidDeviceTest/kotlin/com/squareup/workflow1/android/AndroidRenderWorkflowInTest.kt b/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidRenderWorkflowInTest.kt similarity index 100% rename from workflow-runtime/src/androidDeviceTest/kotlin/com/squareup/workflow1/android/AndroidRenderWorkflowInTest.kt rename to workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidRenderWorkflowInTest.kt diff --git a/workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/android/AndroidRenderWorkflow.kt b/workflow-runtime-android/src/main/java/com/squareup/workflow1/android/AndroidRenderWorkflow.kt similarity index 100% rename from workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/android/AndroidRenderWorkflow.kt rename to workflow-runtime-android/src/main/java/com/squareup/workflow1/android/AndroidRenderWorkflow.kt diff --git a/workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/android/PickledTreesnapshot.kt b/workflow-runtime-android/src/main/java/com/squareup/workflow1/android/PickledTreesnapshot.kt similarity index 100% rename from workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/android/PickledTreesnapshot.kt rename to workflow-runtime-android/src/main/java/com/squareup/workflow1/android/PickledTreesnapshot.kt diff --git a/workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/android/TreeSnapshotSaver.kt b/workflow-runtime-android/src/main/java/com/squareup/workflow1/android/TreeSnapshotSaver.kt similarity index 100% rename from workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/android/TreeSnapshotSaver.kt rename to workflow-runtime-android/src/main/java/com/squareup/workflow1/android/TreeSnapshotSaver.kt diff --git a/workflow-runtime/README.md b/workflow-runtime/README.md index fb16423c54..640411767d 100644 --- a/workflow-runtime/README.md +++ b/workflow-runtime/README.md @@ -5,13 +5,13 @@ This module contains the core APIs and logic for running workflows. ## Kotlin Multiplatform This module is a Kotlin Multiplatform module. The targets currently included for build and test -are `jvm`, `android`, `ios`, and `iosSimulatorSimulatorArm64`. If you are having issues with the -tests, ensure you have the correct version of XCode installed and can launch a simulator as it's -specified in the gradle build file (Currently iPhone 14). +are `jvm`, `ios`, and `iosSimulatorSimulatorArm64`. If you are having issues with the tests, +ensure you have the correct version of XCode installed and can launch a simulator as it's specified +in the gradle build file (Currently iPhone 14). You can also choose to specify your targets for build and test with the property `workflow.targets` -as either `kmp`, `jvm`, `android`, `ios`, `js`. The default is `kmp` (all the targets). Set this in -your global `~/.gradle/gradle.properties` or specify the property in your gradle command, e.g.: +as either `kmp`, `jvm`, `ios`, `js`. The default is `kmp` (all the targets). Set this in your +global `~/.gradle/gradle.properties` or specify the property in your gradle command, e.g.: ```bash ./gradlew build -Pworkflow.targets=jvm diff --git a/workflow-runtime/api/jvm/workflow-runtime.api b/workflow-runtime/api/workflow-runtime.api similarity index 100% rename from workflow-runtime/api/jvm/workflow-runtime.api rename to workflow-runtime/api/workflow-runtime.api diff --git a/workflow-runtime/build.gradle.kts b/workflow-runtime/build.gradle.kts index 89039d4df5..10993c7abf 100644 --- a/workflow-runtime/build.gradle.kts +++ b/workflow-runtime/build.gradle.kts @@ -1,18 +1,12 @@ -import com.android.build.api.dsl.androidLibrary import com.squareup.workflow1.buildsrc.iosWithSimulatorArm64 plugins { id("kotlin-multiplatform") - id("com.android.kotlin.multiplatform.library") id("published") id("app.cash.burst") } kotlin { - // Needed for expect class Lock, which is not public API, so this doesn't add any binary compat - // risk. - compilerOptions.freeCompilerArgs.add("-Xexpect-actual-classes") - val targets = project.findProperty("workflow.targets") ?: "kmp" if (targets == "kmp" || targets == "ios") { iosWithSimulatorArm64() @@ -20,75 +14,19 @@ kotlin { if (targets == "kmp" || targets == "jvm") { jvm {} } - 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() - - withDeviceTestBuilder { - sourceSetTreeName = "test" - }.configure { - instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - - // Disable transition and rotation animations. - animationsDisabled = true - } - } - } if (targets == "kmp" || targets == "js") { js(IR) { browser() } } - sourceSets { - commonMain { - dependencies { - api(project(":workflow-core")) - api(libs.kotlinx.coroutines.core) - } - } - - commonTest { - dependencies { - implementation(libs.kotlinx.coroutines.test.common) - implementation(libs.kotlin.test.core) - } - } - - androidMain { - dependencies { - // Add Android-specific dependencies here. Note that this source set depends on - // commonMain by default and will correctly pull the Android artifacts of any KMP - // dependencies declared in commonMain. - val composeBom = project.dependencies.platform(libs.androidx.compose.bom) - - api(libs.androidx.compose.ui.android) - api(libs.androidx.lifecycle.viewmodel.savedstate) - - implementation(composeBom) - } - } - - getByName("androidDeviceTest") { - dependencies { - implementation(project(":workflow-ui:internal-testing-android")) + // Needed for expect class Lock, which is not public API, so this doesn't add any binary compat + // risk. + compilerOptions.freeCompilerArgs.add("-Xexpect-actual-classes") +} - implementation(libs.androidx.test.espresso.core) - implementation(libs.androidx.test.junit) - implementation(libs.squareup.leakcanary.instrumentation) +dependencies { + commonMainApi(project(":workflow-core")) + commonMainApi(libs.kotlinx.coroutines.core) - implementation(libs.androidx.activity.ktx) - implementation(libs.androidx.lifecycle.viewmodel.ktx) - implementation(libs.androidx.test.core) - implementation(libs.androidx.test.truth) - implementation(libs.kotlin.test.core) - implementation(libs.kotlin.test.jdk) - implementation(libs.kotlinx.coroutines.android) - implementation(libs.kotlinx.coroutines.test) - implementation(libs.squareup.papa) - } - } - } + commonTestImplementation(libs.kotlinx.coroutines.test.common) + commonTestImplementation(libs.kotlin.test.core) } diff --git a/workflow-runtime/src/androidMain/README.md b/workflow-runtime/src/androidMain/README.md deleted file mode 100644 index af4bc6316b..0000000000 --- a/workflow-runtime/src/androidMain/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Android vs JVM targets - -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`. diff --git a/workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/internal/Synchronization.android.kt b/workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/internal/Synchronization.android.kt deleted file mode 100644 index e84a031233..0000000000 --- a/workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/internal/Synchronization.android.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.squareup.workflow1.internal - -internal actual typealias Lock = Any - -internal actual inline fun Lock.withLock(block: () -> R): R = synchronized(this, block) diff --git a/workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/internal/Throwables.android.kt b/workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/internal/Throwables.android.kt deleted file mode 100644 index f4ddf5e2cf..0000000000 --- a/workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/internal/Throwables.android.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.squareup.workflow1.internal - -actual fun T.withKey(stackTraceKey: Any): T = apply { - val realTop = stackTrace[0] - val fakeTop = StackTraceElement( - // Real class name to ensure that we are still "in project". - realTop.className, - "fakeMethodForCrashGrouping", - /* fileName = */ stackTraceKey.toString(), - /* lineNumber = */ stackTraceKey.hashCode() - ) - stackTrace = stackTrace.toMutableList().apply { add(0, fakeTop) }.toTypedArray() -} diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt index db4a6cdaf5..435e2c8667 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt @@ -1,7 +1,6 @@ package com.squareup.workflow1 import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor -import com.squareup.workflow1.WorkflowInterceptor.RuntimeUpdate import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/ReflectionNames.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/ReflectionNames.kt new file mode 100644 index 0000000000..7761301410 --- /dev/null +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/ReflectionNames.kt @@ -0,0 +1,3 @@ +package com.squareup.workflow1 + +expect val ILLEGAL_ARGUMENT_EXCEPTION_NAME: String diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt index bdc4494b25..477326786a 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt @@ -8,7 +8,6 @@ import kotlin.reflect.KType import kotlin.reflect.typeOf import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertTrue import kotlin.test.fail internal class SimpleLoggingWorkflowInterceptorTest { @@ -19,7 +18,7 @@ internal class SimpleLoggingWorkflowInterceptorTest { interceptor.onSessionStarted(scope, TestWorkflowSession) scope.cancel() - assertAllMatch(ErrorLoggingInterceptor.EXPECTED_ERRORS, interceptor.errors) + assertEquals(ErrorLoggingInterceptor.EXPECTED_ERRORS, interceptor.errors) } @Test fun onInitialState_handles_logging_exceptions() { @@ -32,14 +31,14 @@ internal class SimpleLoggingWorkflowInterceptorTest { TestWorkflowSession ) - assertAllMatch(ErrorLoggingInterceptor.EXPECTED_ERRORS, interceptor.errors) + assertEquals(ErrorLoggingInterceptor.EXPECTED_ERRORS, interceptor.errors) } @Test fun onPropsChanged_handles_logging_exceptions() { val interceptor = ErrorLoggingInterceptor() interceptor.onPropsChanged(Unit, Unit, Unit, { _, _, _ -> }, TestWorkflowSession) - assertAllMatch(ErrorLoggingInterceptor.EXPECTED_ERRORS, interceptor.errors) + assertEquals(ErrorLoggingInterceptor.EXPECTED_ERRORS, interceptor.errors) } @Test fun onRender_handles_logging_exceptions() { @@ -53,14 +52,14 @@ internal class SimpleLoggingWorkflowInterceptorTest { TestWorkflowSession, ) - assertAllMatch(ErrorLoggingInterceptor.EXPECTED_ERRORS, interceptor.errors) + assertEquals(ErrorLoggingInterceptor.EXPECTED_ERRORS, interceptor.errors) } @Test fun onSnapshotState_handles_logging_exceptions() { val interceptor = ErrorLoggingInterceptor() interceptor.onSnapshotState(Unit, { null }, TestWorkflowSession) - assertAllMatch(ErrorLoggingInterceptor.EXPECTED_ERRORS, interceptor.errors) + assertEquals(ErrorLoggingInterceptor.EXPECTED_ERRORS, interceptor.errors) } private open class ErrorLoggingInterceptor : SimpleLoggingWorkflowInterceptor() { @@ -76,27 +75,10 @@ internal class SimpleLoggingWorkflowInterceptorTest { companion object { val EXPECTED_ERRORS = listOf( - ( - "ErrorLoggingInterceptor\\.logBeforeMethod threw exception:\n" + - ".+IllegalArgumentException.*" - ).toRegex(), - ( - "ErrorLoggingInterceptor\\.logAfterMethod threw exception:\n" + - ".+IllegalArgumentException.*" - ).toRegex() - ) - } - } - - private fun assertAllMatch( - expected: List, - actual: List - ) { - assertEquals(expected.size, actual.size) - expected.zip(actual).forEachIndexed { index, (expectedPattern, actualString) -> - assertTrue( - expectedPattern.matches(actualString), - "Expected string at index $index to match pattern /$expectedPattern/: \"$actualString\"" + "ErrorLoggingInterceptor.logBeforeMethod threw exception:\n" + + ILLEGAL_ARGUMENT_EXCEPTION_NAME, + "ErrorLoggingInterceptor.logAfterMethod threw exception:\n" + + ILLEGAL_ARGUMENT_EXCEPTION_NAME ) } } diff --git a/workflow-runtime/src/iosTest/kotlin/com/squareup/workflow1/ReflectionNames.kt b/workflow-runtime/src/iosTest/kotlin/com/squareup/workflow1/ReflectionNames.kt new file mode 100644 index 0000000000..29388c00c1 --- /dev/null +++ b/workflow-runtime/src/iosTest/kotlin/com/squareup/workflow1/ReflectionNames.kt @@ -0,0 +1,4 @@ +package com.squareup.workflow1 + +actual val ILLEGAL_ARGUMENT_EXCEPTION_NAME = + IllegalArgumentException::class.qualifiedName.toString() diff --git a/workflow-runtime/src/jsTest/kotlin/com/squareup/workflow1/ReflectionNames.kt b/workflow-runtime/src/jsTest/kotlin/com/squareup/workflow1/ReflectionNames.kt new file mode 100644 index 0000000000..405787e502 --- /dev/null +++ b/workflow-runtime/src/jsTest/kotlin/com/squareup/workflow1/ReflectionNames.kt @@ -0,0 +1,3 @@ +package com.squareup.workflow1 + +actual val ILLEGAL_ARGUMENT_EXCEPTION_NAME = IllegalArgumentException::class.simpleName.toString() diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SystemUtils.jvm.kt b/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SystemUtils.jvm.kt deleted file mode 100644 index 354e7ef9ca..0000000000 --- a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SystemUtils.jvm.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.squareup.workflow1.internal - -internal actual fun currentTimeMillis(): Long = System.currentTimeMillis() diff --git a/workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/internal/SystemUtils.android.kt b/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt similarity index 100% rename from workflow-runtime/src/androidMain/kotlin/com/squareup/workflow1/internal/SystemUtils.android.kt rename to workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/SystemUtils.kt diff --git a/workflow-runtime/src/jvmTest/kotlin/com/squareup/workflow1/ReflectionNames.kt b/workflow-runtime/src/jvmTest/kotlin/com/squareup/workflow1/ReflectionNames.kt new file mode 100644 index 0000000000..29388c00c1 --- /dev/null +++ b/workflow-runtime/src/jvmTest/kotlin/com/squareup/workflow1/ReflectionNames.kt @@ -0,0 +1,4 @@ +package com.squareup.workflow1 + +actual val ILLEGAL_ARGUMENT_EXCEPTION_NAME = + IllegalArgumentException::class.qualifiedName.toString() diff --git a/workflow-ui/core-android/build.gradle.kts b/workflow-ui/core-android/build.gradle.kts index 89adc8b266..1d489d7e81 100644 --- a/workflow-ui/core-android/build.gradle.kts +++ b/workflow-ui/core-android/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { // Needs to be API for the WorkflowInterceptor argument to WorkflowRunner.Config. api(project(":workflow-runtime")) + api(project(":workflow-runtime-android")) api(project(":workflow-ui:core-common")) compileOnly(libs.androidx.viewbinding) 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 2d400d0a8c..4ee8c37f7c 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,9 +1,10 @@ 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 /** @@ -12,23 +13,35 @@ import okio.ByteString * * Read the [Parcelable] back with [toParcelable]. */ -@Deprecated( - "Use toSnapshot() from workflow-core instead.", - replaceWith = ReplaceWith("toSnapshot()", "com.squareup.workflow1.toSnapshot") -) -public fun Parcelable.toSnapshot(): Snapshot = toSnapshot() +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() + } +} /** * @return a [Parcelable] previously wrapped with [toSnapshot], or `null` if the receiver is empty. */ -@Deprecated( - "Use toParcelable() from workflow-core instead.", - replaceWith = ReplaceWith("toParcelable()", "com.squareup.workflow1.toParcelable") -) -public inline fun Snapshot.toParcelable(): T? = toParcelable() +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 ByteString.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 +}