@@ -40,22 +40,21 @@ import kotlinx.coroutines.withContext
4040 * (`AndroidComposeView` + the host recomposer + `Choreographer`).
4141 *
4242 * Two work queues mirror `AndroidUiDispatcher`'s two queues:
43- * - [effectDispatcher ] (Android's `toRunTrampolined`): coroutine dispatch, composition effects
43+ * - [trampolineDispatcher ] (Android's `toRunTrampolined`): coroutine dispatch, composition effects
4444 * (`LaunchedEffect`, `rememberCoroutineScope` launches) and the recomposer's effect context;
45- * - [recomposeDispatcher ] (Android's `toRunOnFrame`), together with [frameClock]: `withFrameNanos`
46- * awaiters and recomposition (the recomposition loop runs on `recomposeDispatcher + frameClock`).
45+ * - [frameDispatcher ] (Android's `toRunOnFrame`), together with [frameClock]: `withFrameNanos`
46+ * awaiters and recomposition (the recomposition loop runs on `frameDispatcher + frameClock`).
4747 *
4848 * Both are [FlushCoroutineDispatcher]s layered over the host's real dispatcher, so on a host with a
4949 * live native loop they drain automatically (like Android's `Handler.post`); [performFrame] and the
50- * scene phases also drain them explicitly via [performScheduledEffects] /
51- * [performScheduledRecomposerTasks].
50+ * scene phases also roll them synchronously via [performTrampolineDispatch] / [performFrameDispatch].
5251 *
5352 * Android drives frames through `Choreographer.doFrame`; non-Android platforms have no such hook, so
5453 * the host calls [performFrame] explicitly before driving scene measure/layout and draw.
5554 *
5655 * The host dispatcher must be confined to a single thread, so [composeThreadId] is stable - the
5756 * analog of Android's UI [android.os.Looper] thread. It is recorded whenever the recomposer runs on
58- * the host thread (via [performScheduledRecomposerTasks ]) and read by [runOnComposeThread].
57+ * the host thread (via [performFrameDispatch ]) and read by [runOnComposeThread].
5958 */
6059@InternalComposeUiApi
6160class FrameRecomposer (
@@ -65,26 +64,32 @@ class FrameRecomposer(
6564 private val job = Job ()
6665 private val coroutineScope = CoroutineScope (coroutineContext + job)
6766
68- /* * Trampoline queue: coroutine dispatch / composition effects / scheduled apply notifications. */
69- private val effectDispatcher = FlushCoroutineDispatcher (coroutineScope)
67+ /* *
68+ * Trampoline queue (Android's `toRunTrampolined`): coroutine dispatch / composition effects /
69+ * scheduled apply notifications. Rolled synchronously by [performTrampolineDispatch].
70+ */
71+ private val trampolineDispatcher = FlushCoroutineDispatcher (coroutineScope)
7072
71- /* * Frame queue: `withFrameNanos` awaiters and recomposition tasks. */
72- private val recomposeDispatcher = FlushCoroutineDispatcher (coroutineScope)
73+ /* *
74+ * Frame queue (Android's `toRunOnFrame`): `withFrameNanos` awaiters and recomposition tasks.
75+ * Rolled synchronously by [performFrameDispatch].
76+ */
77+ private val frameDispatcher = FlushCoroutineDispatcher (coroutineScope)
7378
7479 /* *
7580 * The clock that drives the recomposition loop. Its `withFrameNanos` awaiters are resumed by
7681 * [performFrame]; new awaiters request a frame via [invalidate].
7782 */
7883 private val frameClock = BroadcastFrameClock (::onNewAwaiters)
7984
80- private val recomposer = Recomposer (coroutineContext + job + effectDispatcher )
85+ private val recomposer = Recomposer (coroutineContext + job + trampolineDispatcher )
8186
8287 /* *
8388 * Id of the host (compose) thread. The analog of Android's check
8489 * `handler.looper === Looper.myLooper()`: snapshot-observer callbacks run inline when on this
85- * thread, otherwise they are posted to the shared [effectDispatcher ].
90+ * thread, otherwise they are posted to the shared [trampolineDispatcher ].
8691 *
87- * Recorded only in [performScheduledRecomposerTasks ] - i.e. only where the recomposer actually
92+ * Recorded only in [performFrameDispatch ] - i.e. only where the recomposer actually
8893 * executes on the host thread (driven by [performFrame] each frame, and by `setContent` so the
8994 * thread is established before the first frame) - so a stray off-thread call can never corrupt
9095 * which thread is the compose thread.
@@ -107,7 +112,7 @@ class FrameRecomposer(
107112 " FrameRecomposer requires a ContinuationInterceptor in its coroutineContext"
108113 }
109114 coroutineScope.launch(
110- recomposeDispatcher + frameClock,
115+ frameDispatcher + frameClock,
111116 start = CoroutineStart .UNDISPATCHED
112117 ) {
113118 recomposer.runRecomposeAndApplyChanges()
@@ -146,23 +151,8 @@ class FrameRecomposer(
146151 * the trampoline and run on the next turn (after draw), matching Android.
147152 */
148153 fun performFrame (frameTimeNanos : Long ) {
149- // It's usually handled by [GlobalSnapshotManager], but currently there are a few places
150- // that require synchronous execution, so this guard is for compatibility.
151- Snapshot .sendApplyNotifications()
152-
153- recomposeFrame(frameTimeNanos)
154- }
155-
156- /* *
157- * Advances only the host recomposer and frame clock by one frame at [frameTimeNanos].
158- */
159- private fun recomposeFrame (frameTimeNanos : Long ) {
160154 postponeFrameInvalidation {
161- // Flush composition effects (e.g. LaunchedEffect, coroutines launched in
162- // rememberCoroutineScope()) queued by the previous turn must run before
163- // recomposition tasks and frame-clock awaiters.
164- performScheduledEffects()
165- performScheduledRecomposerTasks()
155+ performFrameDispatch()
166156
167157 frameClock.sendFrame(frameTimeNanos)
168158 }
@@ -174,8 +164,8 @@ class FrameRecomposer(
174164 /* * Returns whether the host still has recomposition or loop work to process. */
175165 fun hasPendingWork (): Boolean =
176166 recomposer.hasPendingWork ||
177- effectDispatcher .hasImmediateTasks() ||
178- recomposeDispatcher .hasImmediateTasks() ||
167+ trampolineDispatcher .hasImmediateTasks() ||
168+ frameDispatcher .hasImmediateTasks() ||
179169 frameClock.hasAwaiters
180170
181171 /* * Cancels the host recomposer and releases host-owned resources. */
@@ -198,7 +188,7 @@ class FrameRecomposer(
198188 * Runs [block] on the compose thread: inline when already on it, otherwise [dispatch]ed onto
199189 * the shared trampoline queue. The non-Android analog of `AndroidComposeView`'s
200190 * `if (uiThread) cmd() else handler.post(cmd)`. This is the executor for owners' snapshot
201- * observers; off-thread commands therefore ride [effectDispatcher ] (one shared queue, like
191+ * observers; off-thread commands therefore ride [trampolineDispatcher ] (one shared queue, like
202192 * Android's single UI looper) and are reflected in [hasPendingWork].
203193 */
204194 internal fun runOnComposeThread (block : () -> Unit ) {
@@ -207,30 +197,40 @@ class FrameRecomposer(
207197
208198 /* *
209199 * Enqueues [block] onto the trampoline queue; it runs on the next loop turn or the next
210- * [performScheduledEffects ]. The non-Android analog of `Handler.post`.
200+ * [performTrampolineDispatch ]. The non-Android analog of `Handler.post`.
211201 */
212202 internal fun dispatch (block : () -> Unit ) {
213- effectDispatcher .dispatch(job, Runnable (block))
203+ trampolineDispatcher .dispatch(job, Runnable (block))
214204 }
215205
216206 /* *
217- * Runs the frame queue (pending `withFrameNanos`/recompose tasks) and records the compose thread:
218- * this is where the recomposer (`runRecomposeAndApplyChanges`) executes on the host thread, so it
219- * is the single place [composeThreadId] is set. Driven by [performFrame] each frame, and by
220- * `BaseComposeScene.setContent` so the compose thread is established before the first frame.
207+ * Synchronously rolls the frame loop: drains the [frameDispatcher] queue (pending
208+ * `withFrameNanos` / recompose tasks) after first rolling the trampoline loop via
209+ * [performTrampolineDispatch]. This is also the single place the recomposer
210+ * (`runRecomposeAndApplyChanges`) executes on the host thread, so it is where [composeThreadId]
211+ * is recorded. Driven by [performFrame] each frame, and by `BaseComposeScene.setContent` so the
212+ * compose thread is established before the first frame.
213+ *
214+ * The non-Android analog of `AndroidUiDispatcher.performFrameDispatch`.
221215 */
222- internal fun performScheduledRecomposerTasks (): Unit =
223- trace(" FrameRecomposer:performScheduledRecomposerTasks " ) {
216+ internal fun performFrameDispatch (): Unit =
217+ trace(" FrameRecomposer:performFrameDispatch " ) {
224218 composeThreadId = getCurrentThreadId()
225- recomposeDispatcher.flush()
219+ performTrampolineDispatch()
220+ frameDispatcher.flush()
226221 }
227222
228223 /* *
229- * Runs the trampoline queue (coroutine dispatch / composition effects / scheduled apply
230- * notifications).
224+ * Synchronously rolls the trampoline loop: first flushes pending snapshot apply notifications
225+ * (so writes made since the last turn are visible to the queued work), then drains the
226+ * [trampolineDispatcher] queue (coroutine dispatch / composition effects).
227+ *
228+ * The non-Android analog of `AndroidUiDispatcher.performTrampolineDispatch`.
231229 */
232- internal fun performScheduledEffects (): Unit =
233- trace(" FrameRecomposer:performScheduledEffects" ) {
234- effectDispatcher.flush()
230+ internal fun performTrampolineDispatch (): Unit =
231+ trace(" FrameRecomposer:performTrampolineDispatch" ) {
232+ Snapshot .sendApplyNotifications()
233+
234+ trampolineDispatcher.flush()
235235 }
236236}
0 commit comments