Skip to content

Commit b9377e6

Browse files
963: Provide optional collectionContext for WorkflowLayout.take
Closes #963
1 parent 9889fb6 commit b9377e6

File tree

3 files changed

+43
-5
lines changed

3 files changed

+43
-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
@@ -200,8 +200,8 @@ public final class com/squareup/workflow1/ui/WorkflowLayout : android/widget/Fra
200200
public final fun start (Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewRegistry;)V
201201
public static synthetic fun start$default (Lcom/squareup/workflow1/ui/WorkflowLayout;Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Landroidx/lifecycle/Lifecycle$State;Lcom/squareup/workflow1/ui/ViewEnvironment;ILjava/lang/Object;)V
202202
public static synthetic fun start$default (Lcom/squareup/workflow1/ui/WorkflowLayout;Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewEnvironment;ILjava/lang/Object;)V
203-
public final fun take (Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Landroidx/lifecycle/Lifecycle$State;)V
204-
public static synthetic fun take$default (Lcom/squareup/workflow1/ui/WorkflowLayout;Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Landroidx/lifecycle/Lifecycle$State;ILjava/lang/Object;)V
203+
public final fun take (Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Landroidx/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)V
204+
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
205205
public final fun update (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
206206
}
207207

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ import androidx.lifecycle.Lifecycle.State
1717
import androidx.lifecycle.Lifecycle.State.STARTED
1818
import androidx.lifecycle.coroutineScope
1919
import androidx.lifecycle.repeatOnLifecycle
20+
import kotlinx.coroutines.CoroutineDispatcher
2021
import kotlinx.coroutines.CoroutineScope
2122
import kotlinx.coroutines.Dispatchers
2223
import kotlinx.coroutines.Job
2324
import kotlinx.coroutines.flow.Flow
2425
import kotlinx.coroutines.flow.launchIn
2526
import kotlinx.coroutines.flow.onEach
2627
import kotlinx.coroutines.launch
28+
import kotlin.coroutines.CoroutineContext
29+
import kotlin.coroutines.EmptyCoroutineContext
2730

2831
/**
2932
* A view that can be driven by a stream of [Screen] renderings passed to its [take] method.
@@ -86,15 +89,26 @@ public class WorkflowLayout(
8689
* Typically this comes from `ComponentActivity.lifecycle` or `Fragment.lifecycle`.
8790
* @param [repeatOnLifecycle] the lifecycle state in which renderings should be actively
8891
* updated. Defaults to STARTED, which is appropriate for Activity and Fragment.
92+
* @param [collectionContext] additional [CoroutineContext] we want for the coroutine that is
93+
* launched to collect the renderings. This should not override the [CoroutineDispatcher][kotlinx.coroutines.CoroutineDispatcher]
94+
* but may include some other instrumentation elements.
8995
*/
96+
@OptIn(ExperimentalStdlibApi::class)
9097
public fun take(
9198
lifecycle: Lifecycle,
9299
renderings: Flow<Screen>,
93-
repeatOnLifecycle: State = STARTED
100+
repeatOnLifecycle: State = STARTED,
101+
collectionContext: CoroutineContext = EmptyCoroutineContext
94102
) {
103+
// We remove the dispatcher as we want to use what is provided by the lifecycle.coroutineScope.
104+
val contextWithoutDispatcher = collectionContext.minusKey(CoroutineDispatcher.Key)
105+
val lifecycleDispatcher = lifecycle.coroutineScope.coroutineContext[CoroutineDispatcher.Key]
95106
// Just like https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
96-
lifecycle.coroutineScope.launch {
107+
lifecycle.coroutineScope.launch(contextWithoutDispatcher) {
97108
lifecycle.repeatOnLifecycle(repeatOnLifecycle) {
109+
require(coroutineContext[CoroutineDispatcher.Key] == lifecycleDispatcher) {
110+
"Collection dispatch should happen on the lifecycle's dispatcher."
111+
}
98112
renderings.collect { show(it) }
99113
}
100114
}

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,24 @@ import android.os.Bundle
55
import android.os.Parcelable
66
import android.util.SparseArray
77
import android.view.View
8+
import androidx.lifecycle.Lifecycle
9+
import androidx.lifecycle.testing.TestLifecycleOwner
810
import androidx.test.core.app.ApplicationProvider
911
import com.google.common.truth.Truth.assertThat
12+
import com.squareup.workflow1.ui.container.WrappedScreen
13+
import kotlinx.coroutines.ExperimentalCoroutinesApi
14+
import kotlinx.coroutines.flow.flowOf
15+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
1016
import org.junit.Test
1117
import org.junit.runner.RunWith
1218
import org.robolectric.RobolectricTestRunner
1319
import org.robolectric.annotation.Config
20+
import kotlin.coroutines.CoroutineContext
1421

1522
@RunWith(RobolectricTestRunner::class)
1623
// SDK 28 required for the four-arg constructor we use in our custom view classes.
1724
@Config(manifest = Config.NONE, sdk = [28])
18-
@OptIn(WorkflowUiExperimentalApi::class)
25+
@OptIn(WorkflowUiExperimentalApi::class, ExperimentalCoroutinesApi::class)
1926
internal class WorkflowLayoutTest {
2027
private val context: Context = ApplicationProvider.getApplicationContext()
2128

@@ -38,4 +45,21 @@ internal class WorkflowLayoutTest {
3845
workflowLayout.restoreHierarchyState(viewState)
3946
// No crash, no bug.
4047
}
48+
49+
@Test fun usesLifecycleDispatcher() {
50+
val lifecycleDispatcher = UnconfinedTestDispatcher()
51+
val collectionContext: CoroutineContext = UnconfinedTestDispatcher()
52+
val testLifecycle = TestLifecycleOwner(
53+
Lifecycle.State.RESUMED,
54+
lifecycleDispatcher
55+
)
56+
57+
workflowLayout.take(
58+
lifecycle = testLifecycle.lifecycle,
59+
renderings = flowOf(WrappedScreen(), WrappedScreen()),
60+
collectionContext = collectionContext
61+
)
62+
63+
// No crash then we safely removed the dispatcher.
64+
}
4165
}

0 commit comments

Comments
 (0)