diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index dcd11bfbda..d0218530cf 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -1,4 +1,5 @@ public final class com/squareup/workflow1/ui/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;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;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;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow; diff --git a/workflow-ui/core-android/build.gradle.kts b/workflow-ui/core-android/build.gradle.kts index 9f634d9f6b..89adc8b266 100644 --- a/workflow-ui/core-android/build.gradle.kts +++ b/workflow-ui/core-android/build.gradle.kts @@ -11,7 +11,10 @@ android { } dependencies { + androidTestImplementation(libs.androidx.activity.ktx) androidTestImplementation(libs.androidx.appcompat) + androidTestImplementation(libs.androidx.lifecycle.viewmodel.ktx) + androidTestImplementation(libs.androidx.lifecycle.viewmodel.savedstate) androidTestImplementation(libs.truth) api(libs.androidx.lifecycle.common) diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/AndroidRenderWorkflowInTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/AndroidRenderWorkflowInTest.kt new file mode 100644 index 0000000000..7c22691645 --- /dev/null +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/AndroidRenderWorkflowInTest.kt @@ -0,0 +1,82 @@ +package com.squareup.workflow1.ui + +import android.widget.FrameLayout +import androidx.activity.ComponentActivity +import androidx.activity.viewModels +import androidx.lifecycle.Lifecycle.State.CREATED +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.test.ext.junit.rules.ActivityScenarioRule +import com.google.common.truth.Truth.assertThat +import com.squareup.workflow1.StatelessWorkflow +import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.StateFlow +import leakcanary.DetectLeaksAfterTestSuccess +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain + +@OptIn(WorkflowUiExperimentalApi::class) +internal class AndroidRenderWorkflowInTest { + @get:Rule val scenarioRule = ActivityScenarioRule(ComponentActivity::class.java) + private val scenario get() = scenarioRule.scenario + + @get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()) + .around(scenarioRule) + .around(IdlingDispatcherRule) + + @Test fun removeWorkflowStateDoesWhatItSaysOnTheTin() { + var job: Job? = null + scenario.onActivity { activity -> + val model: SomeViewModel by activity.viewModels() + val renderings: StateFlow = renderWorkflowIn( + workflow = SomeWorkflow, + scope = model.viewModelScope, + savedStateHandle = model.savedStateHandle + ) + + val layout = WorkflowLayout(activity) + activity.setContentView(layout) + + assertThat(model.savedStateHandle.contains(KEY)).isFalse() + + job = layout.take(activity.lifecycle, renderings) + assertThat(model.savedStateHandle.contains(KEY)).isFalse() + } + + scenario.moveToState(CREATED) + scenario.onActivity { activity -> + val model: SomeViewModel by activity.viewModels() + assertThat(model.savedStateHandle.contains(KEY)).isTrue() + + job?.cancel() + assertThat(model.savedStateHandle.contains(KEY)).isTrue() + + model.savedStateHandle.removeWorkflowState() + assertThat(model.savedStateHandle.contains(KEY)).isFalse() + } + } + + object SomeScreen : AndroidScreen { + override val viewFactory: ScreenViewFactory = + ScreenViewFactory.fromCode { _, initialEnvironment, context, _ -> + ScreenViewHolder( + initialEnvironment, + FrameLayout(context) + ) { _, _ -> } + } + } + + object SomeWorkflow : StatelessWorkflow() { + override fun render( + renderProps: Unit, + context: RenderContext + ): Screen { + return SomeScreen + } + } + + class SomeViewModel(val savedStateHandle: SavedStateHandle) : ViewModel() +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt index feba253806..2ba4defea2 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt @@ -1,5 +1,6 @@ package com.squareup.workflow1.ui +import androidx.annotation.VisibleForTesting import androidx.lifecycle.SavedStateHandle import com.squareup.workflow1.RuntimeConfig import com.squareup.workflow1.RuntimeConfigOptions @@ -290,4 +291,15 @@ public fun renderWorkflowIn( .stateIn(scope, Eagerly, renderingsAndSnapshots.value.rendering) } -private const val KEY = "com.squareup.workflow1.ui.renderWorkflowIn-snapshot" +/** + * Removes state added to the `savedStateHandle` argument of the Android-specific + * overload of [renderWorkflowIn]. For use in obscure cases like swapping between + * different Workflow runtimes in an app. Most apps will not use this function. + */ +@WorkflowUiExperimentalApi +public fun SavedStateHandle.removeWorkflowState() { + remove(KEY) +} + +@VisibleForTesting +internal const val KEY = "com.squareup.workflow1.ui.renderWorkflowIn-snapshot"