Skip to content

Commit 12b674c

Browse files
committed
Introduces SavedStateHandle.removeWorkflowState()
If you're juggling multiple workflow runtimes per `Activity` (if you're the kind of person who needs to cal `Job.cancel` on the `Job` returned from `WorkflowLayout.take`) then you might also need to throw away the state captured by an AndroidX `SavedStateHandler` passed to the Android flavor of `renderWorkflowIn()` to prevent memory leaks.
1 parent b91ad6c commit 12b674c

File tree

3 files changed

+98
-1
lines changed

3 files changed

+98
-1
lines changed

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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.squareup.workflow1.ui
2+
3+
import android.widget.FrameLayout
4+
import androidx.activity.ComponentActivity
5+
import androidx.lifecycle.SavedStateHandle
6+
import androidx.lifecycle.ViewModel
7+
import androidx.test.ext.junit.rules.ActivityScenarioRule
8+
import com.squareup.workflow1.StatelessWorkflow
9+
import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule
10+
import leakcanary.DetectLeaksAfterTestSuccess
11+
import org.junit.Rule
12+
import org.junit.Test
13+
import org.junit.rules.RuleChain
14+
import androidx.activity.viewModels
15+
import androidx.lifecycle.Lifecycle.State.CREATED
16+
import kotlinx.coroutines.flow.StateFlow
17+
import androidx.lifecycle.viewModelScope
18+
import com.google.common.truth.Truth.assertThat
19+
import kotlinx.coroutines.Job
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+
scenario.onActivity { activity ->
33+
val model: SomeViewModel by activity.viewModels()
34+
val renderings: StateFlow<Screen> = renderWorkflowIn(
35+
workflow = SomeWorkflow,
36+
scope = model.viewModelScope,
37+
savedStateHandle = model.savedStateHandle
38+
)
39+
40+
val layout = WorkflowLayout(activity)
41+
activity.setContentView(layout)
42+
43+
assertThat(model.savedStateHandle.contains(KEY)).isFalse()
44+
45+
job = layout.take(activity.lifecycle, renderings)
46+
assertThat(model.savedStateHandle.contains(KEY)).isFalse()
47+
}
48+
49+
scenario.moveToState(CREATED)
50+
scenario.onActivity { activity ->
51+
val model: SomeViewModel by activity.viewModels()
52+
assertThat(model.savedStateHandle.contains(KEY)).isTrue()
53+
54+
job?.cancel()
55+
assertThat(model.savedStateHandle.contains(KEY)).isTrue()
56+
57+
model.savedStateHandle.removeWorkflowState()
58+
assertThat(model.savedStateHandle.contains(KEY)).isFalse()
59+
}
60+
}
61+
62+
object SomeScreen : AndroidScreen<SomeScreen> {
63+
override val viewFactory: ScreenViewFactory<SomeScreen> =
64+
ScreenViewFactory.fromCode { _, initialEnvironment, context, _ ->
65+
ScreenViewHolder(
66+
initialEnvironment,
67+
FrameLayout(context)
68+
) { _, _ -> }
69+
}
70+
}
71+
72+
object SomeWorkflow : StatelessWorkflow<Unit, Nothing, Screen>() {
73+
override fun render(
74+
renderProps: Unit,
75+
context: RenderContext
76+
): Screen {
77+
return SomeScreen
78+
}
79+
}
80+
81+
class SomeViewModel(val savedStateHandle: SavedStateHandle) : ViewModel()
82+
}

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)