diff --git a/.buildscript/configure-android-defaults.gradle b/.buildscript/configure-android-defaults.gradle index a2bb16397e..dc0f1fa440 100644 --- a/.buildscript/configure-android-defaults.gradle +++ b/.buildscript/configure-android-defaults.gradle @@ -1,5 +1,5 @@ android { - compileSdkVersion Versions.targetSdk + compileSdkVersion Versions.compileSdk compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/.buildscript/configure-maven-publish.gradle b/.buildscript/configure-maven-publish.gradle index 0f432b569e..42f6b1ca6f 100644 --- a/.buildscript/configure-maven-publish.gradle +++ b/.buildscript/configure-maven-publish.gradle @@ -2,3 +2,7 @@ apply plugin: 'com.vanniktech.maven.publish' group = GROUP version = VERSION_NAME + +mavenPublish { + sonatypeHost = "S01" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index ee1859b12b..bc53e8ab20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ Change Log ========== +> This file is no longer updated, please visit [Releases](https://github.com/square/workflow-kotlin/releases) instead. + ## Version 1.2.0 _2021-11-10_ diff --git a/RELEASING.md b/RELEASING.md index 77031c2352..0d609c507c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -3,8 +3,6 @@ ## Production Releases --- -1. Merge an update of [the change log](CHANGELOG.md) with the changes since the last release. - 1. Make sure you're on the `main` branch (or fix branch, e.g. `v0.1-fixes`). 1. Confirm that the kotlin build is green before committing any changes @@ -23,14 +21,13 @@ 1. Upload the kotlin artifacts: ```bash - ./gradlew clean build && ./gradlew uploadArchives --no-parallel + ./gradlew clean build && ./gradlew publish --no-parallel ``` Disabling parallelism and daemon sharing is required by the vanniktech maven publish plugin. Without those, the artifacts will be split across multiple (invalid) staging repositories. - (Note that `uploadArchives` is deprecated in favor of `publish`, but `publish` makes bad artifacts.) -1. Close and release the staging repository at https://oss.sonatype.org/#stagingRepositories. +1. Close and release the staging repository at https://s01.oss.sonatype.org/#stagingRepositories. 1. Bump the version - **Kotlin:** Update the `VERSION_NAME` property in `gradle.properties` to the new @@ -51,10 +48,11 @@ 1. Create the release on GitHub: 1. Go to the [Releases](https://github.com/square/workflow-kotlin/releases) page for the GitHub project. - 1. Click "Draft a new release". + 1. Click _Draft a new release_. 1. Enter the tag name you just pushed. - 1. Title the release with the same name as the tag. - 1. Copy & paste the changelog entry for this release into the description. + 1. Click _Auto-generate release notes_. + - Edit the generated notes if you feel the need. + - See [this page](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) if you have an itch to customize how our notes are generated. 1. If this is a pre-release version, check the pre-release box. 1. Hit "Publish release". @@ -112,12 +110,11 @@ To build and install the current version to your local Maven repository (`~/.m2` #### Configuration -In order to deploy artifacts to a Maven repository, you'll need to set 4 properties in your private -Gradle properties file (`~/.gradle/gradle.properties`): +In order to deploy artifacts to `s01.oss.sonatype.org`, you'll need to provide +your credentials via these two properties in your private Gradle properties +file(`~/.gradle/gradle.properties`). ``` -RELEASE_REPOSITORY_URL= -SNAPSHOT_REPOSITORY_URL= mavenCentralPassword= ``` @@ -128,8 +125,8 @@ Double-check that `gradle.properties` correctly contains the `-SNAPSHOT` suffix, snapshot artifacts to Sonatype just like you would for a production release: ```bash -./gradlew clean build && ./gradlew uploadArchives --no-parallel +./gradlew clean build && ./gradlew publish --no-parallel ``` You can verify the artifacts are available by visiting -https://oss.sonatype.org/content/repositories/snapshots/com/squareup/workflow/. +https://s01.oss.sonatype.org/content/repositories/snapshots/com/squareup/workflow1/. diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index bd992f0337..9ad2926bbe 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -4,6 +4,7 @@ import java.util.Locale.US import kotlin.reflect.full.declaredMembers object Versions { + const val compileSdk = 31 const val targetSdk = 30 } @@ -109,7 +110,7 @@ object Dependencies { const val generator = "org.openjdk.jmh:jmh-generator-annprocess:1.32" } - const val mavenPublish = "com.vanniktech:gradle-maven-publish-plugin:0.16.0" + const val mavenPublish = "com.vanniktech:gradle-maven-publish-plugin:0.18.0" const val ktlint = "org.jlleitschuh.gradle:ktlint-gradle:10.1.0" const val lanterna = "com.googlecode.lanterna:lanterna:3.1.1" const val okio = "com.squareup.okio:okio:2.10.0" diff --git a/gradle.properties b/gradle.properties index 0f28ab6cc0..02c896ad0d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,18 +8,20 @@ android.useAndroidX=true systemProp.org.gradle.internal.publish.checksums.insecure=true GROUP=com.squareup.workflow1 -VERSION_NAME=1.3.0-SNAPSHOT +VERSION_NAME=1.4.0-SNAPSHOT -POM_DESCRIPTION=Reactive workflows +POM_DESCRIPTION=Square Workflow POM_URL=https://github.com/square/workflow/ POM_SCM_URL=https://github.com/square/workflow/ POM_SCM_CONNECTION=scm:git:git://github.com/square/workflow.git POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/square/workflow.git -POM_LICENCE_NAME=The Apache Software License, Version 2.0 -POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt -POM_LICENCE_DIST=repo +POM_LICENSE_NAME=The Apache Software License, Version 2.0 +POM_LICENSE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENSE_DIST=repo POM_DEVELOPER_ID=square POM_DEVELOPER_NAME=Square, Inc. +POM_DEVELOPER_URL=https://github.com/square/ +SONATYPE_STAGING_PROFILE=com.squareup diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e750102e09..d2880ba800 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt index 8c8dcb0a38..a3d5be2c97 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt @@ -38,6 +38,7 @@ class Component(context: AppCompatActivity) { @OptIn(ExperimentalTime::class) val clock = Monotonic + @Suppress("DEPRECATION") val vibrator = context.getSystemService(VIBRATOR_SERVICE) as Vibrator val boardLoader = BoardLoader( diff --git a/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/EditTextWorkflow.kt b/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/EditTextWorkflow.kt index 89597c9e9b..c1c1d320f7 100644 --- a/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/EditTextWorkflow.kt +++ b/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/EditTextWorkflow.kt @@ -31,7 +31,7 @@ class EditTextWorkflow : StatefulWorkflow { - state = moveCursor(props, state, 1) - props.text.insertCharAt(state.cursorPosition, key.character!!) + val newText = props.text.insertCharAt(state.cursorPosition, key.character!!) + setOutput(newText) + state = moveCursor(newText, state, 1) } - Backspace -> { - if (props.text.isNotEmpty()) { - state = moveCursor(props, state, -1) - props.text.removeRange(state.cursorPosition - 1, state.cursorPosition) + if (props.text.isNotEmpty() && state.cursorPosition > 0) { + val newText = props.text.removeRange(state.cursorPosition - 1, state.cursorPosition) + setOutput(newText) + state = moveCursor(newText, state, -1) } } - ArrowLeft -> state = moveCursor(props, state, -1) - ArrowRight -> state = moveCursor(props, state, 1) + ArrowLeft -> state = moveCursor(props.text, state, -1) + ArrowRight -> state = moveCursor(props.text, state, 1) else -> { // Nothing to do. } @@ -88,13 +89,15 @@ class EditTextWorkflow : StatefulWorkflow { * * At the end of every render pass, the set of [Worker]s that were requested by the workflow are * compared to the set from the last render pass using this method. Workers are compared by their - * _declared_ type. Equivalent workers are allowed to keep running. New workers are started ([run] - * is called and the returned [Flow] is collected). Old workers are cancelled by cancelling their - * collecting coroutines. Workers for which [doesSameWorkAs] returns false will also be restarted. + * _declared_ [KType] - including generics. Equivalent workers are allowed to keep running. + * New workers are started ([run] is called and the returned [Flow] is collected). Old workers are + * cancelled by cancelling their collecting coroutines. Workers for which [doesSameWorkAs] returns + * false will also be restarted. * - * Implementations of this method should not be based on object identity. For example, a [Worker] - * that performs a network request might check that two workers are requests to the same endpoint - * and have the same request data. + * Implementations of this method should not be based on object identity. Nor do they need to be + * based on anything including in the [KType] - such as generics - as those will already be + * compared by the Workflow Runtime, see [WorkerWorkflow]. + * For example, a [Worker] that performs a network request might check that two workers are + * requests to the same endpoint and have the same request data. * * Most implementations of this method should compare constructor parameters. * @@ -314,6 +317,9 @@ public fun Worker.transform( /** * A generic [Worker] implementation that defines equivalent workers as those having equivalent * [outputType]s. This is used by all the [Worker] builder functions. + * + * Note: We do not override the [doesSameWorkAs] definition here because the [outputType] [KType] + * is already compared as part of the [KType] of the class itself in the Workflow runtime. */ @PublishedApi internal class TypedWorker( diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt index cd5a45b1ff..3b4e74324c 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt @@ -29,6 +29,7 @@ import com.squareup.workflow1.ui.WorkflowViewStub import com.squareup.workflow1.ui.getFactoryForRendering import com.squareup.workflow1.ui.getShowRendering import com.squareup.workflow1.ui.showRendering +import com.squareup.workflow1.ui.start import kotlin.reflect.KClass /** @@ -186,6 +187,8 @@ private fun ViewFactory.asComposeViewFactory() = // we don't have access to that. originalFactory.buildView(rendering, viewEnvironment, context, container = null) .also { view -> + view.start() + // Mirrors the check done in ViewRegistry.buildView. checkNotNull(view.getShowRendering()) { "View.bindShowRendering should have been called for $view, typically by the " + diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalViewContainer.kt b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalViewContainer.kt index 7246739a88..e2b9e7986c 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalViewContainer.kt +++ b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalViewContainer.kt @@ -23,6 +23,7 @@ import com.squareup.workflow1.ui.buildView import com.squareup.workflow1.ui.modal.ModalViewContainer.Companion.binding import com.squareup.workflow1.ui.onBackPressedDispatcherOwnerOrNull import com.squareup.workflow1.ui.showRendering +import com.squareup.workflow1.ui.start import kotlin.reflect.KClass /** @@ -70,6 +71,7 @@ public open class ModalViewContainer @JvmOverloads constructor( container = this ) .apply { + start() // If the modal's root view has no backPressedHandler, add a no-op one to // ensure that the `onBackPressed` call below will not leak up to handlers // that should be blocked by this modal session. diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index 0b2c097cf2..456cd95b6d 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -16,8 +16,8 @@ public abstract interface class com/squareup/workflow1/ui/AndroidScreen : com/sq } public final class com/squareup/workflow1/ui/AndroidViewRegistryKt { - public static final fun buildView (Lcom/squareup/workflow1/ui/ViewRegistry;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;)Landroid/view/View; - public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/ViewRegistry;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Landroid/view/View; + public static final fun buildView (Lcom/squareup/workflow1/ui/ViewRegistry;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;)Landroid/view/View; + public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/ViewRegistry;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;ILjava/lang/Object;)Landroid/view/View; public static final fun getFactoryFor (Lcom/squareup/workflow1/ui/ViewRegistry;Lkotlin/reflect/KClass;)Lcom/squareup/workflow1/ui/ViewFactory; public static final fun getFactoryForRendering (Lcom/squareup/workflow1/ui/ViewRegistry;Ljava/lang/Object;)Lcom/squareup/workflow1/ui/ViewFactory; } @@ -39,19 +39,19 @@ public final class com/squareup/workflow1/ui/BuilderViewFactory : com/squareup/w } public final class com/squareup/workflow1/ui/DecorativeScreenViewFactory : com/squareup/workflow1/ui/ScreenViewFactory { - public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)V - public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)V - public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;)V + public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;)V + public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; public fun getType ()Lkotlin/reflect/KClass; } public final class com/squareup/workflow1/ui/DecorativeViewFactory : com/squareup/workflow1/ui/ViewFactory { - public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)V - public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)V - public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;)V + public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;)V + public synthetic fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/ui/ViewStarter;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun buildView (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; public fun getType ()Lkotlin/reflect/KClass; } @@ -96,9 +96,8 @@ public final class com/squareup/workflow1/ui/ScreenViewFactory$DefaultImpls { } public final class com/squareup/workflow1/ui/ScreenViewFactoryKt { - public static final fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;)Landroid/view/View; - public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Landroid/view/View; - public static final fun showFirstRendering (Landroid/view/View;)V + public static final fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;)Landroid/view/View; + public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;ILjava/lang/Object;)Landroid/view/View; } public abstract interface class com/squareup/workflow1/ui/ScreenViewRunner { @@ -109,21 +108,6 @@ public abstract interface class com/squareup/workflow1/ui/ScreenViewRunner { public final class com/squareup/workflow1/ui/ScreenViewRunner$Companion { } -public final class com/squareup/workflow1/ui/ShowRenderingTag { - public fun (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;)V - public final fun component1 ()Ljava/lang/Object; - public final fun component2 ()Lcom/squareup/workflow1/ui/ViewEnvironment; - public final fun component3 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow1/ui/ShowRenderingTag; - public static synthetic fun copy$default (Lcom/squareup/workflow1/ui/ShowRenderingTag;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/ShowRenderingTag; - public fun equals (Ljava/lang/Object;)Z - public final fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; - public final fun getShowRendering ()Lkotlin/jvm/functions/Function2; - public final fun getShowing ()Ljava/lang/Object; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - public final class com/squareup/workflow1/ui/SnapshotParcelsKt { public static final fun toSnapshot (Landroid/os/Parcelable;)Lcom/squareup/workflow1/Snapshot; } @@ -156,9 +140,13 @@ public final class com/squareup/workflow1/ui/ViewShowRenderingKt { public static final fun bindShowRendering (Landroid/view/View;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;)V public static final fun canShowRendering (Landroid/view/View;Ljava/lang/Object;)Z public static final fun getEnvironment (Landroid/view/View;)Lcom/squareup/workflow1/ui/ViewEnvironment; - public static final fun getRendering (Landroid/view/View;)Ljava/lang/Object; public static final fun getShowRendering (Landroid/view/View;)Lkotlin/jvm/functions/Function2; public static final fun showRendering (Landroid/view/View;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;)V + public static final fun start (Landroid/view/View;)V +} + +public abstract interface class com/squareup/workflow1/ui/ViewStarter { + public abstract fun startView (Landroid/view/View;Lkotlin/jvm/functions/Function0;)V } public final class com/squareup/workflow1/ui/WorkflowLayout : android/widget/FrameLayout { @@ -170,6 +158,42 @@ public final class com/squareup/workflow1/ui/WorkflowLayout : android/widget/Fra public final fun take (Lkotlinx/coroutines/flow/Flow;)V } +public abstract class com/squareup/workflow1/ui/WorkflowViewState { + public abstract fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public abstract fun getShowRendering ()Lkotlin/jvm/functions/Function2; +} + +public final class com/squareup/workflow1/ui/WorkflowViewState$New : com/squareup/workflow1/ui/WorkflowViewState { + public fun (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component2 ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public final fun component3 ()Lkotlin/jvm/functions/Function2; + public final fun component4 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/WorkflowViewState$New; + public static synthetic fun copy$default (Lcom/squareup/workflow1/ui/WorkflowViewState$New;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/WorkflowViewState$New; + public fun equals (Ljava/lang/Object;)Z + public fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public fun getShowRendering ()Lkotlin/jvm/functions/Function2; + public synthetic fun getShowing ()Ljava/lang/Object; + public final fun getStarter ()Lkotlin/jvm/functions/Function1; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/ui/WorkflowViewState$Started : com/squareup/workflow1/ui/WorkflowViewState { + public fun (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;)V + public final fun component2 ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public final fun component3 ()Lkotlin/jvm/functions/Function2; + public final fun copy (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow1/ui/WorkflowViewState$Started; + public static synthetic fun copy$default (Lcom/squareup/workflow1/ui/WorkflowViewState$Started;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/WorkflowViewState$Started; + public fun equals (Ljava/lang/Object;)Z + public fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public fun getShowRendering ()Lkotlin/jvm/functions/Function2; + public synthetic fun getShowing ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/squareup/workflow1/ui/WorkflowViewStub : android/view/View { public fun (Landroid/content/Context;)V public fun (Landroid/content/Context;Landroid/util/AttributeSet;)V diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt index 70685b3770..b14c273762 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeScreenViewFactoryTest.kt @@ -11,7 +11,7 @@ import org.junit.Test internal class DecorativeScreenViewFactoryTest { private val instrumentation = InstrumentationRegistry.getInstrumentation() - @Test fun initializeView_is_only_call_to_showRendering() { + @Test fun viewStarter_is_only_call_to_showRendering() { val events = mutableListOf() val innerViewFactory = object : ScreenViewFactory { @@ -38,27 +38,25 @@ internal class DecorativeScreenViewFactoryTest { val enhancedEnv = env + (envString to "Updated environment") Pair(outer.wrapped, enhancedEnv) }, - initializeView = { - val outerRendering = getRendering() - events += "initializeView $outerRendering ${environment!![envString]}" - showFirstRendering() - events += "exit initializeView" + viewStarter = { view, doStart -> + events += "viewStarter ${view.getRendering()} ${view.environment!![envString]}" + doStart() + events += "exit viewStarter" } ) - val viewRegistry = ViewRegistry(innerViewFactory) + val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) - outerViewFactory.buildView( - OuterRendering("outer", InnerRendering("inner")), + OuterRendering("outer", InnerRendering("inner")).buildView( viewEnvironment, instrumentation.context - ) + ).start() assertThat(events).containsExactly( - "initializeView OuterRendering(outerData=outer, wrapped=InnerRendering(innerData=inner)) " + + "viewStarter OuterRendering(outerData=outer, wrapped=InnerRendering(innerData=inner)) " + "Updated environment", "inner showRendering InnerRendering(innerData=inner)", - "exit initializeView" + "exit viewStarter" ) } @@ -86,14 +84,13 @@ internal class DecorativeScreenViewFactoryTest { innerShowRendering(outerRendering.wrapped, env) } ) - val viewRegistry = ViewRegistry(innerViewFactory) + val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) - outerViewFactory.buildView( - OuterRendering("outer", InnerRendering("inner")), + OuterRendering("outer", InnerRendering("inner")).buildView( viewEnvironment, instrumentation.context - ) + ).start() assertThat(events).containsExactly( "doShowRendering OuterRendering(outerData=outer, wrapped=InnerRendering(innerData=inner))", @@ -101,6 +98,83 @@ internal class DecorativeScreenViewFactoryTest { ) } + // https://github.com/square/workflow-kotlin/issues/597 + @Test fun double_wrapping_only_calls_showRendering_once() { + val events = mutableListOf() + + val innerViewFactory = object : ScreenViewFactory { + override val type = InnerRendering::class + override fun buildView( + initialRendering: InnerRendering, + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? + ): View = InnerView(contextForNewView).apply { + bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> + events += "inner showRendering $rendering" + } + } + } + + val envString = object : ViewEnvironmentKey(String::class) { + override val default: String get() = "Not set" + } + + val outerViewFactory = DecorativeScreenViewFactory( + type = OuterRendering::class, + map = { outer, env -> + val enhancedEnv = env + (envString to "Outer Updated environment" + + " SHOULD NOT SEE THIS! It will be clobbered by WayOutRendering") + Pair(outer.wrapped, enhancedEnv) + }, + viewStarter = { view, doStart -> + events += "outer viewStarter ${view.getRendering()} ${view.environment!![envString]}" + doStart() + events += "exit outer viewStarter" + } + ) + + val wayOutViewFactory = DecorativeScreenViewFactory( + type = WayOutRendering::class, + map = { wayOut, env -> + val enhancedEnv = env + (envString to "Way Out Updated environment triumphs over all") + Pair(wayOut.wrapped, enhancedEnv) + }, + viewStarter = { view, doStart -> + events += "way out viewStarter ${view.getRendering()} ${view.environment!![envString]}" + doStart() + events += "exit way out viewStarter" + } + ) + val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory, wayOutViewFactory) + val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + + WayOutRendering("way out", OuterRendering("outer", InnerRendering("inner"))).buildView( + viewEnvironment, + instrumentation.context + ).start() + + assertThat(events).containsExactly( + "way out viewStarter " + + "WayOutRendering(wayOutData=way out, wrapped=" + + "OuterRendering(outerData=outer, wrapped=" + + "InnerRendering(innerData=inner))) " + + "Way Out Updated environment triumphs over all", + "outer viewStarter " + + // Notice that both the initial rendering and the ViewEnvironment are stomped by + // the outermost wrapper before inners are invoked. Could try to give + // the inner wrapper access to the rendering it expected, but there are no + // use cases and it trashes the API. + "WayOutRendering(wayOutData=way out, wrapped=" + + "OuterRendering(outerData=outer, wrapped=" + + "InnerRendering(innerData=inner))) " + + "Way Out Updated environment triumphs over all", + "inner showRendering InnerRendering(innerData=inner)", + "exit outer viewStarter", + "exit way out viewStarter" + ) + } + @Test fun subsequent_showRendering_calls_wrapped_showRendering() { val events = mutableListOf() @@ -125,14 +199,13 @@ internal class DecorativeScreenViewFactoryTest { innerShowRendering(outerRendering.wrapped, env) } ) - val viewRegistry = ViewRegistry(innerViewFactory) + val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) - val view = outerViewFactory.buildView( - OuterRendering("out1", InnerRendering("in1")), + val view = OuterRendering("out1", InnerRendering("in1")).buildView( viewEnvironment, instrumentation.context - ) + ).apply { start() } events.clear() view.showRendering(OuterRendering("out2", InnerRendering("in2")), viewEnvironment) @@ -149,5 +222,10 @@ internal class DecorativeScreenViewFactoryTest { val wrapped: InnerRendering ) : Screen + private data class WayOutRendering( + val wayOutData: String, + val wrapped: OuterRendering + ) : Screen + private class InnerView(context: Context) : View(context) } diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeViewFactoryTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeViewFactoryTest.kt index 3f02ef81d0..ea88b4134c 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeViewFactoryTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/DecorativeViewFactoryTest.kt @@ -13,7 +13,7 @@ import org.junit.Test internal class DecorativeViewFactoryTest { private val instrumentation = InstrumentationRegistry.getInstrumentation() - @Test fun initializeView_is_only_call_to_showRendering() { + @Test fun viewStarter_is_only_call_to_showRendering() { val events = mutableListOf() val innerViewFactory = object : ViewFactory { @@ -40,27 +40,26 @@ internal class DecorativeViewFactoryTest { val enhancedEnv = env + (envString to "Updated environment") Pair(outer.wrapped, enhancedEnv) }, - initializeView = { - val outerRendering = getRendering() - events += "initializeView $outerRendering ${environment!![envString]}" - showFirstRendering() - events += "exit initializeView" + viewStarter = { view, doStart -> + events += "viewStarter ${view.getRendering()} ${view.environment!![envString]}" + doStart() + events += "exit viewStarter" } ) - val viewRegistry = ViewRegistry(innerViewFactory) + val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) - outerViewFactory.buildView( + viewRegistry.buildView( OuterRendering("outer", InnerRendering("inner")), viewEnvironment, instrumentation.context - ) + ).start() assertThat(events).containsExactly( - "initializeView OuterRendering(outerData=outer, wrapped=InnerRendering(innerData=inner)) " + + "viewStarter OuterRendering(outerData=outer, wrapped=InnerRendering(innerData=inner)) " + "Updated environment", "inner showRendering InnerRendering(innerData=inner)", - "exit initializeView" + "exit viewStarter" ) } @@ -88,14 +87,14 @@ internal class DecorativeViewFactoryTest { innerShowRendering(outerRendering.wrapped, env) } ) - val viewRegistry = ViewRegistry(innerViewFactory) + val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) - outerViewFactory.buildView( + viewRegistry.buildView( OuterRendering("outer", InnerRendering("inner")), viewEnvironment, instrumentation.context - ) + ).start() assertThat(events).containsExactly( "doShowRendering OuterRendering(outerData=outer, wrapped=InnerRendering(innerData=inner))", @@ -103,6 +102,84 @@ internal class DecorativeViewFactoryTest { ) } + // https://github.com/square/workflow-kotlin/issues/597 + @Test fun double_wrapping_only_calls_showRendering_once() { + val events = mutableListOf() + + val innerViewFactory = object : ViewFactory { + override val type = InnerRendering::class + override fun buildView( + initialRendering: InnerRendering, + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? + ): View = InnerView(contextForNewView).apply { + bindShowRendering(initialRendering, initialViewEnvironment) { rendering, _ -> + events += "inner showRendering $rendering" + } + } + } + + val envString = object : ViewEnvironmentKey(String::class) { + override val default: String get() = "Not set" + } + + val outerViewFactory = DecorativeViewFactory( + type = OuterRendering::class, + map = { outer, env -> + val enhancedEnv = env + (envString to "Outer Updated environment" + + " SHOULD NOT SEE THIS! It will be clobbered by WayOutRendering") + Pair(outer.wrapped, enhancedEnv) + }, + viewStarter = { view, doStart -> + events += "outer viewStarter ${view.getRendering()} ${view.environment!![envString]}" + doStart() + events += "exit outer viewStarter" + } + ) + + val wayOutViewFactory = DecorativeViewFactory( + type = WayOutRendering::class, + map = { wayOut, env -> + val enhancedEnv = env + (envString to "Way Out Updated environment triumphs over all") + Pair(wayOut.wrapped, enhancedEnv) + }, + viewStarter = { view, doStart -> + events += "way out viewStarter ${view.getRendering()} ${view.environment!![envString]}" + doStart() + events += "exit way out viewStarter" + } + ) + val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory, wayOutViewFactory) + val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) + + viewRegistry.buildView( + WayOutRendering("way out", OuterRendering("outer", InnerRendering("inner"))), + viewEnvironment, + instrumentation.context + ).start() + + assertThat(events).containsExactly( + "way out viewStarter " + + "WayOutRendering(wayOutData=way out, wrapped=" + + "OuterRendering(outerData=outer, wrapped=" + + "InnerRendering(innerData=inner))) " + + "Way Out Updated environment triumphs over all", + "outer viewStarter " + + // Notice that both the initial rendering and the ViewEnvironment are stomped by + // the outermost wrapper before inners are invoked. Could try to give + // the inner wrapper access to the rendering it expected, but there are no + // use cases and it trashes the API. + "WayOutRendering(wayOutData=way out, wrapped=" + + "OuterRendering(outerData=outer, wrapped=" + + "InnerRendering(innerData=inner))) " + + "Way Out Updated environment triumphs over all", + "inner showRendering InnerRendering(innerData=inner)", + "exit outer viewStarter", + "exit way out viewStarter" + ) + } + @Test fun subsequent_showRendering_calls_wrapped_showRendering() { val events = mutableListOf() @@ -127,14 +204,14 @@ internal class DecorativeViewFactoryTest { innerShowRendering(outerRendering.wrapped, env) } ) - val viewRegistry = ViewRegistry(innerViewFactory) + val viewRegistry = ViewRegistry(innerViewFactory, outerViewFactory) val viewEnvironment = ViewEnvironment(mapOf(ViewRegistry to viewRegistry)) - val view = outerViewFactory.buildView( + val view = viewRegistry.buildView( OuterRendering("out1", InnerRendering("in1")), viewEnvironment, instrumentation.context - ) + ).apply { start() } events.clear() view.showRendering(OuterRendering("out2", InnerRendering("in2")), viewEnvironment) @@ -151,5 +228,10 @@ internal class DecorativeViewFactoryTest { val wrapped: InnerRendering ) + private data class WayOutRendering( + val wayOutData: String, + val wrapped: OuterRendering + ) + private class InnerView(context: Context) : View(context) } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt index 631a14328e..52b765db0a 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt @@ -35,22 +35,27 @@ public fun ViewRegistry.getFactoryFor( } @Suppress("DEPRECATION") -@Deprecated("Use ViewEnvironment.buildview") +@Deprecated("Use Screen.buildview") @WorkflowUiExperimentalApi public fun ViewRegistry.buildView( initialRendering: RenderingT, initialViewEnvironment: ViewEnvironment, contextForNewView: Context, container: ViewGroup? = null, - initializeView: View.() -> Unit = { showFirstRendering() } + viewStarter: ViewStarter? = null, ): View { return getFactoryForRendering(initialRendering).buildView( initialRendering, initialViewEnvironment, contextForNewView, container ).also { view -> - checkNotNull(view.showRenderingTag) { + checkNotNull(view.workflowViewStateOrNull) { "View.bindShowRendering should have been called for $view, typically by the " + - "${ViewFactory::class.java.name} that created it." + "ViewFactory that created it." + } + viewStarter?.let { givenStarter -> + val doStart = view.starter + view.starter = { newView -> + givenStarter.startView(newView) { doStart.invoke(newView) } + } } - initializeView.invoke(view) } } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AsScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AsScreenViewFactory.kt index 2363518a66..3b3e40f8d1 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AsScreenViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AsScreenViewFactory.kt @@ -11,9 +11,7 @@ by ManualScreenViewFactory( initialRendering.rendering, initialViewEnvironment, context, - container, - // Don't call showRendering yet, we need to wrap the function first. - initializeView = { } + container ).also { view -> val legacyShowRendering = view.getShowRendering()!! @@ -21,8 +19,6 @@ by ManualScreenViewFactory( initialRendering, initialViewEnvironment ) { rendering, env -> legacyShowRendering(rendering.rendering, env) } - - view.showFirstRendering() } } ) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeScreenViewFactory.kt index de2b3334cf..bc730bd23d 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeScreenViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeScreenViewFactory.kt @@ -69,9 +69,9 @@ import kotlin.reflect.KClass * by DecorativeScreenViewFactory( * type = WithTutorialTips::class, * map = { withTips -> withTips.wrapped }, - * initializeView = { + * viewStarter = { view, doStart -> * TutorialTipRunner.run(this) - * showFirstRendering>() + * doStart() * } * ) * @@ -109,10 +109,9 @@ import kotlin.reflect.KClass * @param map called to convert instances of [OuterT] to [InnerT], and to * allow [ViewEnvironment] to be transformed. * - * @param initializeView Optional function invoked immediately after the [View] is - * created (that is, immediately after the call to [ScreenViewFactory.buildView]). - * [showRendering], [getRendering] and [environment] are all available when this is called. - * Defaults to a call to [View.showFirstRendering]. + * @param viewStarter An optional wrapper for the function invoked when [View.start] + * is called, allowing for last second initialization of a newly built [View]. + * See [ViewStarter] for details. * * @param doShowRendering called to apply the [ViewShowRendering] function for * [InnerT], allowing pre- and post-processing. Default implementation simply @@ -122,7 +121,7 @@ import kotlin.reflect.KClass public class DecorativeScreenViewFactory( override val type: KClass, private val map: (OuterT, ViewEnvironment) -> Pair, - private val initializeView: View.() -> Unit = { showFirstRendering() }, + private val viewStarter: ViewStarter? = null, private val doShowRendering: ( view: View, innerShowRendering: ViewShowRendering, @@ -140,7 +139,7 @@ public class DecorativeScreenViewFactory( public constructor( type: KClass, map: (OuterT) -> InnerT, - initializeView: View.() -> Unit = { showFirstRendering() }, + viewStarter: ViewStarter? = null, doShowRendering: ( view: View, innerShowRendering: ViewShowRendering, @@ -152,7 +151,7 @@ public class DecorativeScreenViewFactory( ) : this( type, map = { outer, viewEnvironment -> Pair(map(outer), viewEnvironment) }, - initializeView = initializeView, + viewStarter = viewStarter, doShowRendering = doShowRendering ) @@ -168,8 +167,7 @@ public class DecorativeScreenViewFactory( processedInitialEnv, contextForNewView, container, - // Don't call showRendering yet, we need to wrap the function first. - initializeView = { } + viewStarter ) .also { view -> val innerShowRendering: ViewShowRendering = view.getShowRendering()!! @@ -178,8 +176,6 @@ public class DecorativeScreenViewFactory( initialRendering, processedInitialEnv ) { rendering, env -> doShowRendering(view, innerShowRendering, rendering, env) } - - view.initializeView() } } } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeViewFactory.kt index dcd11eeba7..a165c18155 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeViewFactory.kt @@ -11,7 +11,7 @@ import kotlin.reflect.KClass public class DecorativeViewFactory( override val type: KClass, private val map: (OuterT, ViewEnvironment) -> Pair, - private val initializeView: View.() -> Unit = { showFirstRendering() }, + private val viewStarter: ViewStarter? = null, private val doShowRendering: ( view: View, innerShowRendering: ViewShowRendering, @@ -29,7 +29,7 @@ public class DecorativeViewFactory( public constructor( type: KClass, map: (OuterT) -> InnerT, - initializeView: View.() -> Unit = { showFirstRendering() }, + viewStarter: ViewStarter? = null, doShowRendering: ( view: View, innerShowRendering: ViewShowRendering, @@ -41,7 +41,7 @@ public class DecorativeViewFactory( ) : this( type, map = { outer, viewEnvironment -> Pair(map(outer), viewEnvironment) }, - initializeView = initializeView, + viewStarter = viewStarter, doShowRendering = doShowRendering ) @@ -59,8 +59,7 @@ public class DecorativeViewFactory( processedInitialEnv, contextForNewView, container, - // Don't call showRendering yet, we need to wrap the function first. - initializeView = { } + viewStarter ) .also { view -> val innerShowRendering: ViewShowRendering = view.getShowRendering()!! @@ -69,8 +68,6 @@ public class DecorativeViewFactory( initialRendering, processedInitialEnv ) { rendering, env -> doShowRendering(view, innerShowRendering, rendering, env) } - - view.initializeView() } } } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt index 6b9addbcb0..a674dfbbf6 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt @@ -42,29 +42,16 @@ public interface ScreenViewFactory : ViewRegistry.Entry< * It is usually more convenient to use [WorkflowViewStub] or [DecorativeScreenViewFactory] * than to call this method directly. * - * Finds a [ScreenViewFactory] to create a [View] to display [this@buildView]. The new view - * can be updated via calls to [View.showRendering] -- that is, it is guaranteed that - * [bindShowRendering] has been called on this view. + * Finds a [ScreenViewFactory] to create a [View] to display the receiving [Screen]. + * The caller is responsible for calling [View.start] on the new [View]. After that, + * [View.showRendering] can be used to update it with new renderings that + * are [compatible] with this [Screen]. [WorkflowViewStub] takes care of this chore itself. * - * The returned view will have a - * [WorkflowLifecycleOwner][com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner] - * set on it. The returned view must EITHER: + * @param viewStarter An optional wrapper for the function invoked when [View.start] + * is called, allowing for last second initialization of a newly built [View]. + * See [ViewStarter] for details. * - * 1. Be attached at least once to ensure that the lifecycle eventually gets destroyed (because its - * parent is destroyed), or - * 2. Have its - * [WorkflowLifecycleOwner.destroyOnDetach][com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner.destroyOnDetach] - * called, which will either schedule the - * lifecycle to be destroyed if the view is attached, or destroy it immediately if it's detached. - * - * [WorkflowViewStub] takes care of this chore itself. - * - * @param initializeView Optional function invoked immediately after the [View] is - * created (that is, immediately after the call to [ScreenViewFactory.buildView]). - * [showRendering], [getRendering] and [environment] are all available when this is called. - * Defaults to a call to [View.showFirstRendering]. - * - * @throws IllegalArgumentException if no builder can be find for type [ScreenT] + * @throws IllegalArgumentException if no builder can be found for type [ScreenT] * * @throws IllegalStateException if the matching [ScreenViewFactory] fails to call * [View.bindShowRendering] when constructing the view @@ -74,27 +61,41 @@ public fun ScreenT.buildView( viewEnvironment: ViewEnvironment, contextForNewView: Context, container: ViewGroup? = null, - initializeView: View.() -> Unit = { showFirstRendering() } + viewStarter: ViewStarter? = null, ): View { val viewFactory = viewEnvironment.getViewFactoryForRendering(this) return viewFactory.buildView(this, viewEnvironment, contextForNewView, container).also { view -> - checkNotNull(view.showRenderingTag) { + checkNotNull(view.workflowViewStateOrNull) { "View.bindShowRendering should have been called for $view, typically by the " + - "${ScreenViewFactory::class.java.name} that created it." + "ScreenViewFactory that created it." + } + viewStarter?.let { givenStarter -> + val doStart = view.starter + view.starter = { newView -> + givenStarter.startView(newView) { doStart.invoke(newView) } + } } - initializeView.invoke(view) } } /** - * Default implementation for the `initializeView` argument of [Screen.buildView], - * and for [DecorativeScreenViewFactory.initializeView]. Calls [showRendering] against - * [getRendering] and [environment]. + * A wrapper for the function invoked when [View.start] is called, allowing for + * last second initialization of a newly built [View]. Provided via [Screen.buildView] + * or [DecorativeScreenViewFactory.viewStarter]. + * + * While [View.getRendering] may be called from [startView], it is not safe to + * assume that the type of the rendering retrieved matches the type the view was + * originally built to display. [ScreenViewFactory] instances can be wrapped, and + * renderings can be mapped to other types. */ @WorkflowUiExperimentalApi -public fun View.showFirstRendering() { - showRendering(getRendering()!!, environment!!) +public fun interface ViewStarter { + /** Called from [View.start]. [doStart] must be invoked. */ + public fun startView( + view: View, + doStart: () -> Unit + ) } @WorkflowUiExperimentalApi diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewLaunchWhenAttached.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewLaunchWhenAttached.kt index ce9fc48221..1f97566810 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewLaunchWhenAttached.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewLaunchWhenAttached.kt @@ -1,5 +1,6 @@ package com.squareup.workflow1.ui +import android.content.res.Resources.NotFoundException import android.view.View import android.view.View.NO_ID import androidx.lifecycle.ViewTreeLifecycleOwner @@ -95,8 +96,13 @@ private fun View.ensureAttachedScope(): AttachedScope { val coroutineName = buildString { append("${view::class.java.name}@${view.hashCode()}") if (view.id != NO_ID) { - append('-') - append(resources.getResourceEntryName(view.id)) + try { + val name = resources.getResourceEntryName(view.id) + append('-') + append(name) + } catch (e: NotFoundException) { + // Ignore. It's just a debugging name, who cares. + } } }.let(::CoroutineName) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewShowRendering.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewShowRendering.kt index 9d21324903..9b9a9d295e 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewShowRendering.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewShowRendering.kt @@ -1,32 +1,31 @@ package com.squareup.workflow1.ui import android.view.View - -@WorkflowUiExperimentalApi -public typealias ViewShowRendering = - (@UnsafeVariance RenderingT, ViewEnvironment) -> Unit +import com.squareup.workflow1.ui.WorkflowViewState.New +import com.squareup.workflow1.ui.WorkflowViewState.Started /** -` * View tag that holds the function to make the view show instances of [RenderingT], and - * the [current rendering][showing]. - * - * @param showing the current rendering. Used by [canShowRendering] to decide if the - * view can be updated with the next rendering. + * Function attached to a view created by [ViewFactory], to allow it + * to respond to [View.showRendering]. */ @WorkflowUiExperimentalApi -public data class ShowRenderingTag( - val showing: RenderingT, - val environment: ViewEnvironment, - val showRendering: ViewShowRendering -) +public typealias ViewShowRendering = + (@UnsafeVariance RenderingT, ViewEnvironment) -> Unit +// Unsafe because typealias ViewShowRendering is not supported, can't +// declare variance on a typealias. If I recall correctly. /** - * Establishes [showRendering] as the implementation of [View.showRendering] - * for the receiver, possibly replacing the existing one. Likewise sets / updates - * the values returned by [View.getRendering] and [View.environment]. + * For use by implementations of [ViewFactory.buildView]. Establishes [showRendering] + * as the implementation of [View.showRendering] for the receiver, possibly replacing + * the existing one. * - * Intended for use by implementations of [ViewFactory.buildView]. + * - After this method is called, [View.start] must be called exactly + * once before [View.showRendering] can be called. + * - If this method is called again _after_ [View.start] (e.g. if a [View] is reused), + * the receiver is reset to its initialized state, and [View.start] must + * be called again. * + * @see ViewFactory * @see DecorativeViewFactory */ @WorkflowUiExperimentalApi @@ -35,31 +34,56 @@ public fun View.bindShowRendering( initialViewEnvironment: ViewEnvironment, showRendering: ViewShowRendering ) { - setTag( - R.id.view_show_rendering_function, - ShowRenderingTag(initialRendering, initialViewEnvironment, showRendering) - ) + workflowViewState = when (workflowViewStateOrNull) { + is New<*> -> New(initialRendering, initialViewEnvironment, showRendering, starter) + else -> New(initialRendering, initialViewEnvironment, showRendering) + } + + // Note that if there is already a `New<*>` tag, we have to take care to propagate + // the starter. Repeated calls happen whenever one ViewFactory delegates to another. + // + // - We render `NamedScreen(FooScreen())` + // - The view is built by `FooScreenFactory`, which calls `bindShowRendering()` + // - `NamedScreenFactory` invokes `FooScreenFactory.buildView`, and calls + // `bindShowRendering>()` on the view that `FooScreenFactory` built. +} + +/** + * Note that [WorkflowViewStub] calls this method for you. + * + * Makes the initial call to [View.showRendering], along with any wrappers that have been + * added via [ViewRegistry.buildView], or [DecorativeViewFactory.viewStarter]. + * + * - It is an error to call this method more than once. + * - It is an error to call [View.showRendering] without having called this method first. + */ +@WorkflowUiExperimentalApi +public fun View.start() { + val current = workflowViewStateAsNew + workflowViewState = Started(current.showing, current.environment, current.showRendering) + current.starter(this) } /** - * It is usually more convenient to use [WorkflowViewStub] than to call this method directly. + * Note that [WorkflowViewStub.showRendering] makes this check for you. * * True if this view is able to show [rendering]. * - * Returns `false` if [bindShowRendering] has not been called, so it is always safe to - * call this method. Otherwise returns the [compatibility][compatible] of the initial - * [rendering] and the new one. + * Returns `false` if [View.bindShowRendering] has not been called, so it is always safe to + * call this method. Otherwise returns the [compatibility][compatible] of the current + * [View.getRendering] and the new one. */ @WorkflowUiExperimentalApi public fun View.canShowRendering(rendering: Any): Boolean { - return getRendering()?.matches(rendering) == true + return getRendering()?.let { compatible(it, rendering) } == true } /** - * It is usually more convenient to use [WorkflowViewStub] than to call this method directly. + * It is usually more convenient to call [WorkflowViewStub.showRendering] + * than to call this method directly. * - * Sets the workflow rendering associated with this view, and displays it by - * invoking the [ViewShowRendering] function previously set by [bindShowRendering]. + * Shows [rendering] in this View by invoking the [ViewShowRendering] function + * previously set by [bindShowRendering]. * * @throws IllegalStateException if [bindShowRendering] has not been called. */ @@ -68,47 +92,42 @@ public fun View.showRendering( rendering: RenderingT, viewEnvironment: ViewEnvironment ) { - showRenderingTag - ?.let { tag -> - check(tag.showing.matches(rendering)) { - "Expected $this to be able to show rendering $rendering, but that did not match " + - "previous rendering ${tag.showing}. " + - "Consider using ${WorkflowViewStub::class.java.simpleName} to display arbitrary types." - } - - // Update the tag's rendering and viewEnvironment. - bindShowRendering(rendering, viewEnvironment, tag.showRendering) - // And do the actual showRendering work. - tag.showRendering.invoke(rendering, viewEnvironment) + workflowViewStateAsStarted.let { viewState -> + check(compatible(viewState.showing, rendering)) { + "Expected $this to be able to show rendering $rendering, but that did not match " + + "previous rendering ${viewState.showing}. " + + "Consider using WorkflowViewStub to display arbitrary types." } - ?: error( - "Expected $this to have a showRendering function to show $rendering. " + - "Perhaps it was not built by a ScreenViewFactory, " + - "or perhaps the factory did not call View.bindShowRendering." - ) + + // Update the tag's rendering and viewEnvironment before calling + // the actual showRendering function. + workflowViewState = Started(rendering, viewEnvironment, viewState.showRendering) + viewState.showRendering.invoke(rendering, viewEnvironment) + } } /** - * Returns the most recent rendering shown by this view, or null if [bindShowRendering] - * has never been called. + * Returns the most recent rendering shown by this view cast to [RenderingT], + * or null if [bindShowRendering] has never been called. + * + * @throws ClassCastException if the current rendering is not of type [RenderingT] */ @WorkflowUiExperimentalApi -public fun View.getRendering(): RenderingT? { +public inline fun View.getRendering(): RenderingT? { // Can't use a val because of the parameter type. - @Suppress("UNCHECKED_CAST") - return when (val showing = showRenderingTag?.showing) { + return when (val showing = workflowViewStateOrNull?.showing) { null -> null else -> showing as RenderingT } } /** - * Returns the most recent [ViewEnvironment] that apply to this view, or null if [bindShowRendering] + * Returns the most recent [ViewEnvironment] applied to this view, or null if [bindShowRendering] * has never been called. */ @WorkflowUiExperimentalApi public val View.environment: ViewEnvironment? - get() = showRenderingTag?.environment + get() = workflowViewStateOrNull?.environment /** * Returns the function set by the most recent call to [bindShowRendering], or null @@ -116,17 +135,12 @@ public val View.environment: ViewEnvironment? */ @WorkflowUiExperimentalApi public fun View.getShowRendering(): ViewShowRendering? { - return showRenderingTag?.showRendering + return workflowViewStateOrNull?.showRendering } -/** - * Returns the [ShowRenderingTag] established by the last call to [View.bindShowRendering], - * or null if that method has never been called. - */ -@WorkflowUiExperimentalApi -@PublishedApi -internal val View.showRenderingTag: ShowRenderingTag<*>? - get() = getTag(R.id.view_show_rendering_function) as? ShowRenderingTag<*> - @WorkflowUiExperimentalApi -private fun Any.matches(other: Any) = compatible(this, other) +internal var View.starter: (View) -> Unit + get() = workflowViewStateAsNew.starter + set(value) { + workflowViewState = workflowViewStateAsNew.copy(starter = value) + } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewState.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewState.kt new file mode 100644 index 0000000000..a0d42a28c7 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewState.kt @@ -0,0 +1,58 @@ +package com.squareup.workflow1.ui + +import android.view.View +import com.squareup.workflow1.ui.WorkflowViewState.New +import com.squareup.workflow1.ui.WorkflowViewState.Started + +/** + * [View tag][View.setTag] that holds the functions and state backing [View.showRendering], etc. + */ +@WorkflowUiExperimentalApi +@PublishedApi +internal sealed class WorkflowViewState { + @PublishedApi + internal abstract val showing: RenderingT + abstract val environment: ViewEnvironment + abstract val showRendering: ViewShowRendering + + /** [bindShowRendering] has been called, [start] has not. */ + data class New( + override val showing: RenderingT, + override val environment: ViewEnvironment, + override val showRendering: ViewShowRendering, + + val starter: (View) -> Unit = { view -> + view.showRendering(view.getRendering()!!, view.environment!!) + } + ) : WorkflowViewState() + + /** [start] has been called. It's safe to call [showRendering] now. */ + data class Started( + override val showing: RenderingT, + override val environment: ViewEnvironment, + override val showRendering: ViewShowRendering + ) : WorkflowViewState() +} + +@WorkflowUiExperimentalApi +@PublishedApi +internal val View.workflowViewStateOrNull: WorkflowViewState<*>? + get() = getTag(R.id.workflow_ui_view_state) as? WorkflowViewState<*> + +@WorkflowUiExperimentalApi +internal var View.workflowViewState: WorkflowViewState<*> + get() = workflowViewStateOrNull ?: error( + "Expected $this to have been built by a ViewFactory. " + + "Perhaps the factory did not call View.bindShowRendering." + ) + set(value) = setTag(R.id.workflow_ui_view_state, value) + +@WorkflowUiExperimentalApi internal val View.workflowViewStateAsNew: New<*> + get() = workflowViewState as? New<*> ?: error( + "Expected $this to be un-started, but View.start() has been called" + ) + +@WorkflowUiExperimentalApi internal val View.workflowViewStateAsStarted: Started<*> + get() = workflowViewState as? Started<*> ?: error( + "Expected $this to have been started, but View.start() has not been called" + ) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewStub.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewStub.kt index 06fb08405a..46557974a0 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewStub.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewStub.kt @@ -196,7 +196,7 @@ public class WorkflowViewStub @JvmOverloads constructor( * * @return the view that showed [rendering] * - * @throws IllegalArgumentException if no binding can be find for the type of [rendering] + * @throws IllegalArgumentException if no binding can be found for the type of [rendering] * * @throws IllegalStateException if the matching * [ViewFactory][com.squareup.workflow1.ui.ViewFactory] fails to call @@ -234,12 +234,14 @@ public class WorkflowViewStub @JvmOverloads constructor( viewEnvironment, parent.context, parent, - initializeView = { - WorkflowLifecycleOwner.installOn(this) - showFirstRendering() + viewStarter = { view, doStart -> + WorkflowLifecycleOwner.installOn(view) + doStart() } ) .also { newView -> + newView.start() + if (inflatedId != NO_ID) newView.id = inflatedId if (updatesVisibility) newView.visibility = visibility background?.let { newView.background = it } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt index 83f1b745a2..be81a2590f 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt @@ -26,8 +26,8 @@ import com.squareup.workflow1.ui.compatible import com.squareup.workflow1.ui.container.BackStackConfig.First import com.squareup.workflow1.ui.container.BackStackConfig.Other import com.squareup.workflow1.ui.container.ViewStateCache.SavedState -import com.squareup.workflow1.ui.showFirstRendering import com.squareup.workflow1.ui.showRendering +import com.squareup.workflow1.ui.start /** * A container view that can display a stream of [BackStackScreen] instances. @@ -96,14 +96,15 @@ public open class BackStackContainer @JvmOverloads constructor( } val newView = named.top.buildView( - viewEnvironment = environment, - contextForNewView = this.context, - container = this, - initializeView = { - WorkflowLifecycleOwner.installOn(this) - showFirstRendering() - } + viewEnvironment = environment, + contextForNewView = this.context, + container = this, + viewStarter = { view, doStart -> + WorkflowLifecycleOwner.installOn(view) + doStart() + } ) + newView.start() viewStateCache.update(named.backStack, oldViewMaybe, newView) val popped = currentRendering?.backStack?.any { compatible(it, named.top) } == true diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt index d1d2a4ffff..8b37b6f6fe 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt @@ -19,7 +19,9 @@ import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.WorkflowViewStub import com.squareup.workflow1.ui.bindShowRendering -import com.squareup.workflow1.ui.showFirstRendering +import com.squareup.workflow1.ui.environment +import com.squareup.workflow1.ui.getRendering +import com.squareup.workflow1.ui.showRendering import kotlinx.coroutines.flow.MutableStateFlow @WorkflowUiExperimentalApi @@ -95,7 +97,7 @@ internal class BodyAndModalsContainer @JvmOverloads constructor( viewTreeObserver.addOnGlobalLayoutListener(boundsListener) // Ugly, but here in case a strange parent detaches and re-attaches us. // https://github.com/square/workflow-kotlin/issues/314 - showFirstRendering() + showRendering(getRendering()!!, environment!!) } override fun onDetachedFromWindow() { diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt index c1f052ed79..6cdc8045e2 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt @@ -17,6 +17,7 @@ import com.squareup.workflow1.ui.backPressedHandler import com.squareup.workflow1.ui.buildView import com.squareup.workflow1.ui.environment import com.squareup.workflow1.ui.showRendering +import com.squareup.workflow1.ui.start import java.lang.IllegalStateException import kotlin.reflect.KClass @@ -63,6 +64,7 @@ public abstract class ModalScreenOverlayDialogFactory>( context: Context ): Dialog { val contentView = initialRendering.content.buildView(initialEnvironment, context).apply { + start() // If the content view has no backPressedHandler, add a no-op one to // ensure that the `onBackPressed` call below will not leak up to handlers // that should be blocked by this modal session. diff --git a/workflow-ui/core-android/src/main/res/values/ids.xml b/workflow-ui/core-android/src/main/res/values/ids.xml index 0f1c293999..7bb8e9e4ef 100644 --- a/workflow-ui/core-android/src/main/res/values/ids.xml +++ b/workflow-ui/core-android/src/main/res/values/ids.xml @@ -6,7 +6,7 @@ --> - + diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/LegacyAndroidViewRegistryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/LegacyAndroidViewRegistryTest.kt index 3ce5c3d5a8..2fcabed911 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/LegacyAndroidViewRegistryTest.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/LegacyAndroidViewRegistryTest.kt @@ -32,7 +32,8 @@ internal class LegacyAndroidViewRegistryTest { } assertThat(error.message).isEqualTo( "A ViewFactory should have been registered to display " + - "render this, bud, or that class should implement AndroidViewRendering.") + "render this, bud, or that class should implement AndroidViewRendering." + ) } @Test fun `getFactoryFor delegates to composite registries`() { @@ -136,6 +137,7 @@ internal class LegacyAndroidViewRegistryTest { private object ViewRendering : AndroidViewRendering { override val viewFactory: TestViewFactory = TestViewFactory(ViewRendering::class) } + private val overrideViewRenderingFactory = TestViewFactory(ViewRendering::class) private class TestRegistry(private val factories: Map, ViewFactory<*>>) : ViewRegistry { @@ -166,8 +168,8 @@ internal class LegacyAndroidViewRegistryTest { called = true return mock { on { - getTag(eq(com.squareup.workflow1.ui.R.id.view_show_rendering_function)) - } doReturn (ShowRenderingTag(initialRendering, initialViewEnvironment, { _, _ -> })) + getTag(eq(com.squareup.workflow1.ui.R.id.workflow_ui_view_state)) + } doReturn (WorkflowViewState.New(initialRendering, initialViewEnvironment, { _, _ -> })) } } } diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ScreenViewFactoryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ScreenViewFactoryTest.kt index 50c546b20d..d650b901f9 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ScreenViewFactoryTest.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ScreenViewFactoryTest.kt @@ -74,8 +74,8 @@ internal class ScreenViewFactoryTest { return mock { on { - getTag(eq(R.id.view_show_rendering_function)) - } doReturn (ShowRenderingTag(initialRendering, initialViewEnvironment, { _, _ -> })) + getTag(eq(com.squareup.workflow1.ui.R.id.workflow_ui_view_state)) + } doReturn (WorkflowViewState.New(initialRendering, initialViewEnvironment, { _, _ -> })) } } } diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewLaunchWhenAttachedTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewLaunchWhenAttachedTest.kt index e0664c6c73..5e7e3500c3 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewLaunchWhenAttachedTest.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewLaunchWhenAttachedTest.kt @@ -1,5 +1,6 @@ package com.squareup.workflow1.ui +import android.content.res.Resources.NotFoundException import android.view.View import android.view.View.OnAttachStateChangeListener import androidx.lifecycle.Lifecycle.Event.ON_DESTROY @@ -177,6 +178,34 @@ internal class ViewLaunchWhenAttachedTest { assertThat(coroutineName).contains("${view.hashCode()}") } + @Test fun `launchWhenAttached includes view id name in coroutine name`() { + var coroutineName: String? = null + mockAttachedToWindow(view, true) + whenever(view.resources.getResourceEntryName(anyInt())).thenReturn("fnord") + + // Action: launch coroutine! + view.launchWhenAttached { + coroutineName = coroutineContext[CoroutineName]?.name + } + + assertThat(coroutineName).contains("fnord") + } + + @Test fun `launchWhenAttached tolerates garbage ids`() { + var coroutineName: String? = null + mockAttachedToWindow(view, true) + whenever(view.resources.getResourceEntryName(anyInt())).thenThrow(NotFoundException()) + + // Action: launch coroutine! + view.launchWhenAttached { + coroutineName = coroutineContext[CoroutineName]?.name + } + + assertThat(coroutineName).isNotNull() + assertThat(coroutineName).contains("android.view.View") + assertThat(coroutineName).contains("${view.hashCode()}") + } + private fun performViewAttach() { mockAttachedToWindow(view, true) verify(view).addOnAttachStateChangeListener(onAttachStateChangeListener.capture())