Skip to content

Commit a90bf43

Browse files
committed
Return the Job from WorkflowLayout.take so it can be canceled.
1 parent 7316b0d commit a90bf43

File tree

5 files changed

+70
-7
lines changed

5 files changed

+70
-7
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ public final class com/squareup/workflow1/ui/WorkflowLayout : android/widget/Fra
128128
public synthetic fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
129129
public final fun show (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
130130
public static synthetic fun show$default (Lcom/squareup/workflow1/ui/WorkflowLayout;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;ILjava/lang/Object;)V
131-
public final fun take (Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Landroidx/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)V
132-
public static synthetic fun take$default (Lcom/squareup/workflow1/ui/WorkflowLayout;Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Landroidx/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)V
131+
public final fun take (Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Landroidx/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/Job;
132+
public static synthetic fun take$default (Lcom/squareup/workflow1/ui/WorkflowLayout;Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Landroidx/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/Job;
133133
}
134134

135135
public final class com/squareup/workflow1/ui/WorkflowViewStub : android/view/View {

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.lifecycle.repeatOnLifecycle
1717
import com.squareup.workflow1.ui.androidx.OnBackPressedDispatcherOwnerKey
1818
import com.squareup.workflow1.ui.androidx.WorkflowAndroidXSupport.onBackPressedDispatcherOwnerOrNull
1919
import kotlinx.coroutines.CoroutineDispatcher
20+
import kotlinx.coroutines.Job
2021
import kotlinx.coroutines.flow.Flow
2122
import kotlinx.coroutines.launch
2223
import kotlin.coroutines.CoroutineContext
@@ -91,19 +92,23 @@ public class WorkflowLayout(
9192
* @param [collectionContext] additional [CoroutineContext] we want for the coroutine that is
9293
* launched to collect the renderings. This should not override the [CoroutineDispatcher][kotlinx.coroutines.CoroutineDispatcher]
9394
* but may include some other instrumentation elements.
95+
*
96+
* @return the [Job] started to collect [renderings], to allow callers to
97+
* [cancel][Job.cancel] collection -- e.g., before calling [take] again with a new
98+
* [renderings] flow
9499
*/
95100
@OptIn(ExperimentalStdlibApi::class)
96101
public fun take(
97102
lifecycle: Lifecycle,
98103
renderings: Flow<Screen>,
99104
repeatOnLifecycle: State = STARTED,
100105
collectionContext: CoroutineContext = EmptyCoroutineContext
101-
) {
106+
): Job {
102107
// We remove the dispatcher as we want to use what is provided by the lifecycle.coroutineScope.
103108
val contextWithoutDispatcher = collectionContext.minusKey(CoroutineDispatcher.Key)
104109
val lifecycleDispatcher = lifecycle.coroutineScope.coroutineContext[CoroutineDispatcher.Key]
105110
// Just like https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
106-
lifecycle.coroutineScope.launch(contextWithoutDispatcher) {
111+
return lifecycle.coroutineScope.launch(contextWithoutDispatcher) {
107112
lifecycle.repeatOnLifecycle(repeatOnLifecycle) {
108113
require(coroutineContext[CoroutineDispatcher.Key] == lifecycleDispatcher) {
109114
"Collection dispatch should happen on the lifecycle's dispatcher."

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,15 @@ public class WorkflowViewStub @JvmOverloads constructor(
230230

231231
holder = rendering.toViewFactory(viewEnvironment)
232232
.startShowing(rendering, viewEnvironment, parent.context, parent) { view, doStart ->
233-
WorkflowLifecycleOwner.installOn(view, viewEnvironment.onBackPressedDispatcherOwner(parent))
233+
try {
234+
WorkflowLifecycleOwner.installOn(
235+
view,
236+
viewEnvironment.onBackPressedDispatcherOwner(parent)
237+
)
238+
} catch ( t: Throwable) {
239+
println(t.toString())
240+
throw t
241+
}
234242
doStart()
235243
}.apply {
236244
val newView = view

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ public interface WorkflowLifecycleOwner : LifecycleOwner {
8686
onBackPressedDispatcherOwner: OnBackPressedDispatcherOwner,
8787
findParentLifecycle: (View) -> Lifecycle = this::findParentViewTreeLifecycle
8888
) {
89-
RealWorkflowLifecycleOwner(findParentLifecycle).also {
89+
val wlo = RealWorkflowLifecycleOwner(findParentLifecycle)
90+
wlo.also {
9091
view.setViewTreeOnBackPressedDispatcherOwner(onBackPressedDispatcherOwner)
9192
view.setViewTreeLifecycleOwner(it)
9293
view.addOnAttachStateChangeListener(it)

workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/WorkflowLayoutTest.kt

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@ import android.os.Bundle
55
import android.os.Parcelable
66
import android.util.SparseArray
77
import android.view.View
8+
import androidx.activity.OnBackPressedDispatcher
89
import androidx.activity.OnBackPressedDispatcherOwner
10+
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
11+
import androidx.core.view.get
912
import androidx.lifecycle.Lifecycle
1013
import androidx.lifecycle.testing.TestLifecycleOwner
1114
import androidx.test.core.app.ApplicationProvider
1215
import com.google.common.truth.Truth.assertThat
1316
import com.squareup.workflow1.ui.androidx.OnBackPressedDispatcherOwnerKey
1417
import com.squareup.workflow1.ui.navigation.WrappedScreen
1518
import kotlinx.coroutines.ExperimentalCoroutinesApi
19+
import kotlinx.coroutines.flow.MutableSharedFlow
1620
import kotlinx.coroutines.flow.flowOf
1721
import kotlinx.coroutines.test.UnconfinedTestDispatcher
22+
import kotlinx.coroutines.test.runTest
1823
import org.junit.Test
1924
import org.junit.runner.RunWith
2025
import org.robolectric.RobolectricTestRunner
@@ -28,7 +33,13 @@ import kotlin.coroutines.CoroutineContext
2833
internal class WorkflowLayoutTest {
2934
private val context: Context = ApplicationProvider.getApplicationContext()
3035

31-
private val workflowLayout = WorkflowLayout(context).apply { id = 42 }
36+
private val workflowLayout = WorkflowLayout(context).apply {
37+
id = 42
38+
setViewTreeOnBackPressedDispatcherOwner(object : OnBackPressedDispatcherOwner {
39+
override fun getOnBackPressedDispatcher(): OnBackPressedDispatcher { error("yeah no") }
40+
override val lifecycle: Lifecycle get() = error("nope")
41+
})
42+
}
3243

3344
@Test fun ignoresAlienViewState() {
3445
val weirdView = BundleSavingView(context)
@@ -91,6 +102,44 @@ internal class WorkflowLayoutTest {
91102
// No crash then we safely removed the dispatcher.
92103
}
93104

105+
@Test fun takes() {
106+
val lifecycleDispatcher = UnconfinedTestDispatcher()
107+
val testLifecycle = TestLifecycleOwner(
108+
initialState = Lifecycle.State.RESUMED,
109+
coroutineDispatcher = lifecycleDispatcher
110+
)
111+
val flow = MutableSharedFlow<Screen>()
112+
113+
runTest(lifecycleDispatcher) {
114+
val job = workflowLayout.take(
115+
lifecycle = testLifecycle.lifecycle,
116+
renderings = flow,
117+
)
118+
assertThat(workflowLayout[0]).isInstanceOf(WorkflowViewStub::class.java)
119+
flow.emit(WrappedScreen())
120+
assertThat(workflowLayout[0]).isNotInstanceOf(WorkflowViewStub::class.java)
121+
}
122+
}
123+
124+
@Test fun canStopTaking() {
125+
val lifecycleDispatcher = UnconfinedTestDispatcher()
126+
val testLifecycle = TestLifecycleOwner(
127+
initialState = Lifecycle.State.RESUMED,
128+
coroutineDispatcher = lifecycleDispatcher
129+
)
130+
val flow = MutableSharedFlow<Screen>()
131+
132+
runTest(lifecycleDispatcher) {
133+
val job = workflowLayout.take(
134+
lifecycle = testLifecycle.lifecycle,
135+
renderings = flow,
136+
)
137+
job.cancel()
138+
flow.emit(WrappedScreen())
139+
assertThat(workflowLayout[0]).isInstanceOf(WorkflowViewStub::class.java)
140+
}
141+
}
142+
94143
private class BundleSavingView(context: Context) : View(context) {
95144
var saved = false
96145

0 commit comments

Comments
 (0)