Skip to content

Commit a62f80d

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

File tree

3 files changed

+60
-5
lines changed

3 files changed

+60
-5
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: 8 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,24 @@ 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 give callers the option to
97+
* [cancel][Job.cancel] collection -- e.g., before calling [take] again with a new
98+
* [renderings] flow. In most cases the caller can ignore this, interacting with
99+
* the [Job] is very unusual.
94100
*/
95101
@OptIn(ExperimentalStdlibApi::class)
96102
public fun take(
97103
lifecycle: Lifecycle,
98104
renderings: Flow<Screen>,
99105
repeatOnLifecycle: State = STARTED,
100106
collectionContext: CoroutineContext = EmptyCoroutineContext
101-
) {
107+
): Job {
102108
// We remove the dispatcher as we want to use what is provided by the lifecycle.coroutineScope.
103109
val contextWithoutDispatcher = collectionContext.minusKey(CoroutineDispatcher.Key)
104110
val lifecycleDispatcher = lifecycle.coroutineScope.coroutineContext[CoroutineDispatcher.Key]
105111
// Just like https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
106-
lifecycle.coroutineScope.launch(contextWithoutDispatcher) {
112+
return lifecycle.coroutineScope.launch(contextWithoutDispatcher) {
107113
lifecycle.repeatOnLifecycle(repeatOnLifecycle) {
108114
require(coroutineContext[CoroutineDispatcher.Key] == lifecycleDispatcher) {
109115
"Collection dispatch should happen on the lifecycle's dispatcher."

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+
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)