Skip to content

Commit 278b16b

Browse files
authored
Merge pull request #1227 from square/ray/scrub-viewmodel-state
Introduces `SavedStateHandle.removeWorkflowState()`
2 parents a5a9618 + c905ad1 commit 278b16b

File tree

4 files changed

+107
-1
lines changed

4 files changed

+107
-1
lines changed

workflow-ui/core-android/api/core-android.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
public final class com/squareup/workflow1/ui/AndroidRenderWorkflowKt {
2+
public static final fun removeWorkflowState (Landroidx/lifecycle/SavedStateHandle;)V
23
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;
34
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;
45
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;

workflow-ui/core-android/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ android {
1111
}
1212

1313
dependencies {
14+
androidTestImplementation(libs.androidx.activity.ktx)
1415
androidTestImplementation(libs.androidx.appcompat)
16+
androidTestImplementation(libs.androidx.lifecycle.viewmodel.ktx)
17+
androidTestImplementation(libs.androidx.lifecycle.viewmodel.savedstate)
1518
androidTestImplementation(libs.truth)
1619

1720
api(libs.androidx.lifecycle.common)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.squareup.workflow1.ui
2+
3+
import android.widget.FrameLayout
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.viewModels
6+
import androidx.lifecycle.Lifecycle.State.CREATED
7+
import androidx.lifecycle.SavedStateHandle
8+
import androidx.lifecycle.ViewModel
9+
import androidx.lifecycle.viewModelScope
10+
import androidx.test.ext.junit.rules.ActivityScenarioRule
11+
import com.google.common.truth.Truth.assertThat
12+
import com.squareup.workflow1.StatelessWorkflow
13+
import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule
14+
import kotlinx.coroutines.Job
15+
import kotlinx.coroutines.flow.StateFlow
16+
import leakcanary.DetectLeaksAfterTestSuccess
17+
import org.junit.Rule
18+
import org.junit.Test
19+
import org.junit.rules.RuleChain
20+
21+
@OptIn(WorkflowUiExperimentalApi::class)
22+
internal class AndroidRenderWorkflowInTest {
23+
@get:Rule val scenarioRule = ActivityScenarioRule(ComponentActivity::class.java)
24+
private val scenario get() = scenarioRule.scenario
25+
26+
@get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
27+
.around(scenarioRule)
28+
.around(IdlingDispatcherRule)
29+
30+
@Test fun removeWorkflowStateDoesWhatItSaysOnTheTin() {
31+
var job: Job? = null
32+
33+
// Activity.onCreate(), the take() call won't start pulling yet.
34+
scenario.onActivity { activity ->
35+
val model: SomeViewModel by activity.viewModels()
36+
val renderings: StateFlow<Screen> = renderWorkflowIn(
37+
workflow = SomeWorkflow,
38+
scope = model.viewModelScope,
39+
savedStateHandle = model.savedStateHandle
40+
)
41+
42+
val layout = WorkflowLayout(activity)
43+
activity.setContentView(layout)
44+
45+
assertThat(model.savedStateHandle.contains(KEY)).isFalse()
46+
47+
job = layout.take(activity.lifecycle, renderings)
48+
assertThat(model.savedStateHandle.contains(KEY)).isFalse()
49+
}
50+
51+
// Exit onCreate() and move to CREATED status. take() starts to draw
52+
// and the renderWorkflowIn() call above starts pushing TreeSnapshots
53+
// (lazy serialization functions) to the SavedStateHandle.
54+
scenario.moveToState(CREATED)
55+
scenario.onActivity { activity ->
56+
val model: SomeViewModel by activity.viewModels()
57+
assertThat(model.savedStateHandle.contains(KEY)).isTrue()
58+
59+
// The Job returned from take() is canceled. There is still a
60+
// TreeSnapshot and whatever pointer it captured in the SavedStateHandle.
61+
job?.cancel()
62+
assertThat(model.savedStateHandle.contains(KEY)).isTrue()
63+
64+
// We can remove it.
65+
model.savedStateHandle.removeWorkflowState()
66+
assertThat(model.savedStateHandle.contains(KEY)).isFalse()
67+
}
68+
}
69+
70+
object SomeScreen : AndroidScreen<SomeScreen> {
71+
override val viewFactory: ScreenViewFactory<SomeScreen> =
72+
ScreenViewFactory.fromCode { _, initialEnvironment, context, _ ->
73+
ScreenViewHolder(
74+
initialEnvironment,
75+
FrameLayout(context)
76+
) { _, _ -> }
77+
}
78+
}
79+
80+
object SomeWorkflow : StatelessWorkflow<Unit, Nothing, Screen>() {
81+
override fun render(
82+
renderProps: Unit,
83+
context: RenderContext
84+
): Screen {
85+
return SomeScreen
86+
}
87+
}
88+
89+
class SomeViewModel(val savedStateHandle: SavedStateHandle) : ViewModel()
90+
}

workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidRenderWorkflow.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.squareup.workflow1.ui
22

3+
import androidx.annotation.VisibleForTesting
34
import androidx.lifecycle.SavedStateHandle
45
import com.squareup.workflow1.RuntimeConfig
56
import com.squareup.workflow1.RuntimeConfigOptions
@@ -290,4 +291,15 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
290291
.stateIn(scope, Eagerly, renderingsAndSnapshots.value.rendering)
291292
}
292293

293-
private const val KEY = "com.squareup.workflow1.ui.renderWorkflowIn-snapshot"
294+
/**
295+
* Removes state added to the `savedStateHandle` argument of the Android-specific
296+
* overload of [renderWorkflowIn]. For use in obscure cases like swapping between
297+
* different Workflow runtimes in an app. Most apps will not use this function.
298+
*/
299+
@WorkflowUiExperimentalApi
300+
public fun SavedStateHandle.removeWorkflowState() {
301+
remove<Any>(KEY)
302+
}
303+
304+
@VisibleForTesting
305+
internal const val KEY = "com.squareup.workflow1.ui.renderWorkflowIn-snapshot"

0 commit comments

Comments
 (0)