Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit0d757f2

Browse files
Collect renderings on context specified in WorkflowLayout.take
1 parent719cd4a commit0d757f2

File tree

4 files changed

+116
-28
lines changed

4 files changed

+116
-28
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,6 @@ dependencies {
5252
testImplementation(libs.robolectric)
5353
testImplementation(libs.robolectric.annotations)
5454
testImplementation(libs.truth)
55+
56+
androidTestImplementation(libs.androidx.lifecycle.testing)
5557
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
packagecom.squareup.workflow1.ui
2+
3+
importandroid.view.View
4+
importandroidx.lifecycle.Lifecycle
5+
importandroidx.lifecycle.testing.TestLifecycleOwner
6+
importandroidx.test.platform.app.InstrumentationRegistry
7+
importcom.google.common.truth.Truth.assertThat
8+
importkotlinx.coroutines.Dispatchers
9+
importkotlinx.coroutines.ExperimentalCoroutinesApi
10+
importkotlinx.coroutines.flow.MutableStateFlow
11+
importorg.junit.Test
12+
importjava.util.concurrent.CountDownLatch
13+
importjava.util.concurrent.TimeUnit
14+
15+
/**
16+
* Instrumented tests for [WorkflowLayout] that require a real Android environment.
17+
* These tests verify behavior that cannot be properly tested with Robolectric,
18+
* such as the main thread requirement for collecting renderings.
19+
*/
20+
@OptIn(ExperimentalCoroutinesApi::class)
21+
internalclassWorkflowLayoutInstrumentedTest {
22+
23+
@Test
24+
funthrowsWhenCollectingOnBackgroundThread() {
25+
26+
var exception:Throwable?=null
27+
val countDownLatch=CountDownLatch(1)
28+
29+
Thread.setDefaultUncaughtExceptionHandler { _, throwable->
30+
exception= throwable
31+
countDownLatch.countDown()
32+
}
33+
34+
val testLifeCycleOwner=TestLifecycleOwner(Lifecycle.State.CREATED)
35+
val renderings=MutableStateFlow(TestScreen())
36+
37+
val nonMainThreadDispatcher=Dispatchers.IO
38+
39+
val workflowLayout=WorkflowLayout(InstrumentationRegistry.getInstrumentation().context)
40+
workflowLayout.take(
41+
lifecycle= testLifeCycleOwner.lifecycle,
42+
renderings= renderings,
43+
collectionContext= nonMainThreadDispatcher
44+
)
45+
46+
// start the lifecycle.
47+
testLifeCycleOwner.lifecycle.currentState=Lifecycle.State.STARTED
48+
49+
countDownLatch.await(1000,TimeUnit.MILLISECONDS)
50+
51+
assertThat(exception).isNotNull()
52+
assertThat(exception).isInstanceOf(IllegalArgumentException::class.java)
53+
assertThat(exception?.message).contains("Collection dispatch must happen on the main thread!")
54+
}
55+
56+
/**
57+
* Simple test screen for instrumented tests.
58+
*/
59+
privateclassTestScreen :AndroidScreen<TestScreen> {
60+
overrideval viewFactory=
61+
ScreenViewFactory.fromCode<TestScreen> { _, initialEnvironment, context, _->
62+
ScreenViewHolder(initialEnvironment,View(context)) { _, _-> }
63+
}
64+
}
65+
}

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.squareup.workflow1.ui
22

33
importandroid.content.Context
44
importandroid.os.Build.VERSION
5+
importandroid.os.Looper
56
importandroid.os.Parcel
67
importandroid.os.Parcelable
78
importandroid.os.Parcelable.Creator
@@ -16,10 +17,10 @@ import androidx.lifecycle.coroutineScope
1617
importandroidx.lifecycle.repeatOnLifecycle
1718
importcom.squareup.workflow1.ui.androidx.OnBackPressedDispatcherOwnerKey
1819
importcom.squareup.workflow1.ui.androidx.WorkflowAndroidXSupport.onBackPressedDispatcherOwnerOrNull
19-
importkotlinx.coroutines.CoroutineDispatcher
2020
importkotlinx.coroutines.Job
2121
importkotlinx.coroutines.flow.Flow
2222
importkotlinx.coroutines.launch
23+
importkotlinx.coroutines.withContext
2324
importkotlin.coroutines.CoroutineContext
2425
importkotlin.coroutines.EmptyCoroutineContext
2526

@@ -89,8 +90,8 @@ public class WorkflowLayout(
8990
* @param [repeatOnLifecycle] the lifecycle state in which renderings should be actively
9091
* updated. Defaults to STARTED, which is appropriate for Activity and Fragment.
9192
* @param [collectionContext] additional [CoroutineContext] we want for the coroutine that is
92-
* launched to collect the renderings. This should not override the [CoroutineDispatcher][kotlinx.coroutines.CoroutineDispatcher]
93-
*but may include some other instrumentation elements.
93+
* launched to collect the renderings, can include a different dispatcher - but we verify that
94+
*it is a main thread dispatcher since we are updating views here!
9495
*
9596
* @return the [Job] started to collect [renderings], to give callers the option to
9697
* [cancel][Job.cancel] collection -- e.g., before calling [take] again with a new
@@ -104,16 +105,15 @@ public class WorkflowLayout(
104105
repeatOnLifecycle:State =STARTED,
105106
collectionContext:CoroutineContext =EmptyCoroutineContext
106107
):Job {
107-
// We remove the dispatcher as we want to use what is provided by the lifecycle.coroutineScope.
108-
val contextWithoutDispatcher= collectionContext.minusKey(CoroutineDispatcher.Key)
109-
val lifecycleDispatcher= lifecycle.coroutineScope.coroutineContext[CoroutineDispatcher.Key]
110108
// Just like https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
111-
return lifecycle.coroutineScope.launch(contextWithoutDispatcher) {
109+
return lifecycle.coroutineScope.launch {
112110
lifecycle.repeatOnLifecycle(repeatOnLifecycle) {
113-
require(coroutineContext[CoroutineDispatcher.Key]== lifecycleDispatcher) {
114-
"Collection dispatch should happen on the lifecycle's dispatcher."
111+
withContext(collectionContext) {
112+
require(Looper.myLooper()==Looper.getMainLooper()) {
113+
"Collection dispatch must happen on the main thread!"
114+
}
115+
renderings.collect { show(it) }
115116
}
116-
renderings.collect { show(it) }
117117
}
118118
}
119119
}

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

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ import com.squareup.workflow1.ui.androidx.OnBackPressedDispatcherOwnerKey
1717
importcom.squareup.workflow1.ui.navigation.WrappedScreen
1818
importkotlinx.coroutines.ExperimentalCoroutinesApi
1919
importkotlinx.coroutines.flow.MutableSharedFlow
20-
importkotlinx.coroutines.flow.flowOf
20+
importkotlinx.coroutines.flow.onEach
2121
importkotlinx.coroutines.test.UnconfinedTestDispatcher
2222
importkotlinx.coroutines.test.runTest
2323
importorg.junit.Test
2424
importorg.junit.runner.RunWith
2525
importorg.robolectric.RobolectricTestRunner
2626
importorg.robolectric.annotation.Config
27+
importkotlin.coroutines.AbstractCoroutineContextElement
2728
importkotlin.coroutines.CoroutineContext
29+
importkotlin.coroutines.coroutineContext
2830

2931
@RunWith(RobolectricTestRunner::class)
3032
// SDK 28 required for the four-arg constructor we use in our custom view classes.
@@ -92,23 +94,6 @@ internal class WorkflowLayoutTest {
9294
unoriginal.show(BScreen(), env)
9395
}
9496

95-
@TestfunusesLifecycleDispatcher() {
96-
val lifecycleDispatcher=UnconfinedTestDispatcher()
97-
val collectionContext:CoroutineContext=UnconfinedTestDispatcher()
98-
val testLifecycle=TestLifecycleOwner(
99-
Lifecycle.State.RESUMED,
100-
lifecycleDispatcher
101-
)
102-
103-
workflowLayout.take(
104-
lifecycle= testLifecycle.lifecycle,
105-
renderings= flowOf(WrappedScreen(),WrappedScreen()),
106-
collectionContext= collectionContext
107-
)
108-
109-
// No crash then we safely removed the dispatcher.
110-
}
111-
11297
@Testfuntakes() {
11398
val lifecycleDispatcher=UnconfinedTestDispatcher()
11499
val testLifecycle=TestLifecycleOwner(
@@ -147,6 +132,35 @@ internal class WorkflowLayoutTest {
147132
}
148133
}
149134

135+
@TestfunusesProvidedCoroutineContext() {
136+
val lifecycleDispatcher=UnconfinedTestDispatcher()
137+
val testLifecycle=TestLifecycleOwner(
138+
initialState=Lifecycle.State.RESUMED,
139+
coroutineDispatcher= lifecycleDispatcher
140+
)
141+
val flow=MutableSharedFlow<Screen>()
142+
143+
val testElement=TestContextElement()
144+
145+
val trackedFlow= flow.onEach {
146+
if (coroutineContext[TestContextElement]!=null) {
147+
testElement.wasUsed=true
148+
}
149+
}
150+
151+
runTest(lifecycleDispatcher) {
152+
workflowLayout.take(
153+
lifecycle= testLifecycle.lifecycle,
154+
renderings= trackedFlow,
155+
collectionContext= testElement
156+
)
157+
158+
flow.emit(WrappedScreen())
159+
160+
assertThat(testElement.wasUsed).isTrue()
161+
}
162+
}
163+
150164
privateclassBundleSavingView(context:Context) : View(context) {
151165
var saved=false
152166

@@ -166,4 +180,11 @@ internal class WorkflowLayoutTest {
166180
id=42
167181
}
168182
}
183+
184+
privateclassTestContextElement :AbstractCoroutineContextElement(Key) {
185+
companionobject Key : CoroutineContext.Key<TestContextElement>
186+
187+
@Volatile
188+
var wasUsed=false
189+
}
169190
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp