Skip to content

Commit be1fe79

Browse files
Treehugger RobotGerrit Code Review
Treehugger Robot
authored and
Gerrit Code Review
committed
Merge "Support wrapping AndroidUiDispatcher" into androidx-main
2 parents bafed9b + 5ad515a commit be1fe79

File tree

3 files changed

+75
-4
lines changed

3 files changed

+75
-4
lines changed

compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt

+64
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,18 @@ import androidx.test.filters.FlakyTest
3232
import androidx.test.filters.LargeTest
3333
import androidx.test.platform.app.InstrumentationRegistry
3434
import androidx.test.uiautomator.UiDevice
35+
import java.util.concurrent.atomic.AtomicInteger
36+
import kotlin.coroutines.ContinuationInterceptor
37+
import kotlin.coroutines.CoroutineContext
38+
import kotlin.test.fail
3539
import kotlinx.coroutines.CompletableDeferred
40+
import kotlinx.coroutines.CoroutineDispatcher
41+
import kotlinx.coroutines.CoroutineStart
3642
import kotlinx.coroutines.Dispatchers
43+
import kotlinx.coroutines.Runnable
3744
import kotlinx.coroutines.launch
3845
import kotlinx.coroutines.runBlocking
46+
import kotlinx.coroutines.withContext
3947
import kotlinx.coroutines.withTimeoutOrNull
4048
import org.junit.Assert.assertEquals
4149
import org.junit.Assert.assertNotEquals
@@ -174,4 +182,60 @@ class AndroidUiDispatcherTest {
174182
}
175183
)
176184
}
185+
186+
/**
187+
* Test that an AndroidUiDispatcher can be wrapped by another ContinuationInterceptor
188+
* without breaking the MonotonicFrameClock's ability to coordinate with its
189+
* original dispatcher.
190+
*
191+
* Construct a situation where the Choreographer contains three frame callbacks:
192+
* 1) checkpoint 1
193+
* 2) the AndroidUiDispatcher awaiting-frame callback
194+
* 3) checkpoint 2
195+
* Confirm that a call to withFrameNanos made *after* these three frame callbacks
196+
* are enqueued runs *before* checkpoint 2, indicating that it ran with the original
197+
* dispatcher's awaiting-frame callback, even though we wrapped the dispatcher.
198+
*/
199+
@Test
200+
fun wrappedDispatcherPostsToDispatcherFrameClock() = runBlocking(Dispatchers.Main) {
201+
val uiDispatcherContext = AndroidUiDispatcher.Main
202+
val uiDispatcher = uiDispatcherContext[ContinuationInterceptor] as CoroutineDispatcher
203+
val wrapperDispatcher = object : CoroutineDispatcher() {
204+
override fun dispatch(context: CoroutineContext, block: Runnable) {
205+
uiDispatcher.dispatch(context, block)
206+
}
207+
}
208+
209+
val choreographer = Choreographer.getInstance()
210+
211+
val expectCount = AtomicInteger(1)
212+
fun expect(value: Int) {
213+
while (true) {
214+
val old = expectCount.get()
215+
if (old != value) fail("expected sequence $old but encountered $value")
216+
if (expectCount.compareAndSet(value, value + 1)) break
217+
}
218+
}
219+
220+
choreographer.postFrameCallback {
221+
expect(1)
222+
}
223+
224+
launch(uiDispatcherContext, start = CoroutineStart.UNDISPATCHED) {
225+
withFrameNanos {
226+
expect(2)
227+
}
228+
}
229+
230+
choreographer.postFrameCallback {
231+
expect(4)
232+
}
233+
234+
withContext(uiDispatcherContext + wrapperDispatcher) {
235+
withFrameNanos {
236+
expect(3)
237+
}
238+
expect(5)
239+
}
240+
}
177241
}

compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ class AndroidUiDispatcher private constructor(
131131
* A [MonotonicFrameClock] associated with this [AndroidUiDispatcher]'s [choreographer]
132132
* that may be used to await [Choreographer] frame dispatch.
133133
*/
134-
val frameClock: MonotonicFrameClock = AndroidUiFrameClock(choreographer)
134+
val frameClock: MonotonicFrameClock = AndroidUiFrameClock(choreographer, this)
135135

136136
override fun dispatch(context: CoroutineContext, block: Runnable) {
137137
synchronized(lock) {

compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiFrameClock.android.kt

+10-3
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,20 @@ import kotlinx.coroutines.suspendCancellableCoroutine
2121
import kotlin.coroutines.ContinuationInterceptor
2222
import kotlin.coroutines.coroutineContext
2323

24-
class AndroidUiFrameClock(
25-
val choreographer: Choreographer
24+
class AndroidUiFrameClock internal constructor(
25+
val choreographer: Choreographer,
26+
private val dispatcher: AndroidUiDispatcher?
2627
) : androidx.compose.runtime.MonotonicFrameClock {
28+
29+
constructor(
30+
choreographer: Choreographer
31+
) : this(choreographer, null)
32+
2733
override suspend fun <R> withFrameNanos(
2834
onFrame: (Long) -> R
2935
): R {
30-
val uiDispatcher = coroutineContext[ContinuationInterceptor] as? AndroidUiDispatcher
36+
val uiDispatcher = dispatcher
37+
?: coroutineContext[ContinuationInterceptor] as? AndroidUiDispatcher
3138
return suspendCancellableCoroutine { co ->
3239
// Important: this callback won't throw, and AndroidUiDispatcher counts on it.
3340
val callback = Choreographer.FrameCallback { frameTimeNanos ->

0 commit comments

Comments
 (0)