How Compose Synchronizes with Android's Choreographer

skydovesJaewoong Eum (skydoves)||11 min read

How Compose Synchronizes with Android's Choreographer

Every frame in a Compose application starts with a signal from the Android Choreographer. The Choreographer is the system component that schedules work to run in sync with the display's VSYNC pulse, ensuring that UI updates happen at a consistent cadence (typically 60 or 120 frames per second). Compose does not manage its own frame timing. Instead, it uses a layered abstraction that bridges the Choreographer's frame callbacks into Compose's coroutine based runtime: AndroidUiFrameClock suspends a coroutine until the next VSYNC, AndroidUiDispatcher coordinates dispatch with both a Handler and the Choreographer, the Recomposer aligns recomposition with frame boundaries via BroadcastFrameClock, and PausableMonotonicFrameClock ties all of it to the Activity lifecycle.

In this article, you'll explore the MonotonicFrameClock interface that defines frame timing in Compose, how AndroidUiFrameClock bridges Choreographer.FrameCallback to coroutine suspension, how AndroidUiDispatcher combines a Handler and Choreographer into a single dispatcher, how BroadcastFrameClock distributes frame times from the Recomposer to animations and other awaiters, how the Recomposer uses these clocks to sequence recomposition within a frame, and how PausableMonotonicFrameClock pauses frame delivery when the Activity is stopped.

The fundamental problem: Coroutines need frame timing

Compose's runtime is built on Kotlin coroutines. Recomposition, animations, and layout all execute as coroutine code. But coroutines have no built in concept of "frames." A coroutine can suspend and resume at any time, while a smooth UI requires work to be synchronized with the display's refresh cycle.

The Choreographer provides this synchronization on Android. It receives a VSYNC signal from the display hardware and dispatches frame callbacks at the beginning of each frame. Any code that wants to run in sync with the display registers a callback with Choreographer.postFrameCallback() and receives the frame timestamp when it fires.

The challenge is bridging these two models. Compose needs a way for coroutines to say "suspend me until the next frame" and for the Choreographer to wake them up at the right time. This is what the frame clock abstraction provides.

MonotonicFrameClock: The frame timing interface

MonotonicFrameClock is a CoroutineContext.Element that defines a single operation: suspend until the next frame, run a callback with the frame timestamp, and return the result:

public interface MonotonicFrameClock : CoroutineContext.Element {
    public suspend fun <R> withFrameNanos(
        onFrame: (frameTimeNanos: Long) -> R
    ): R

    public companion object Key : CoroutineContext.Key<MonotonicFrameClock>
}

Because it is a CoroutineContext.Element, a MonotonicFrameClock can be installed into a coroutine context just like a dispatcher. Any coroutine running in that context can call withFrameNanos to synchronize with the frame.

The onFrame callback receives the frame timestamp in nanoseconds. This timestamp is not necessarily "now." It may be the target time for the frame, which is what the Choreographer provides. The value is strictly monotonically increasing, so it can be used to calculate animation deltas between frames.

A convenience extension provides millisecond granularity:

public suspend inline fun <R> MonotonicFrameClock.withFrameMillis(
    crossinline onFrame: (frameTimeMillis: Long) -> R
): R = withFrameNanos { onFrame(it / 1_000_000L) }

AndroidUiFrameClock: Bridging Choreographer to coroutines

AndroidUiFrameClock is the Android implementation of MonotonicFrameClock. It converts a Choreographer frame callback into a coroutine resumption. The withFrameNanos method suspends the calling coroutine using suspendCancellableCoroutine, registers a Choreographer.FrameCallback, and resumes the coroutine when the next VSYNC arrives:

class AndroidUiFrameClock internal constructor(
    val choreographer: Choreographer,
    private val dispatcher: AndroidUiDispatcher?,
) : MonotonicFrameClock {

    override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R {
        val uiDispatcher = dispatcher
            ?: coroutineContext[ContinuationInterceptor] as? AndroidUiDispatcher
        return suspendCancellableCoroutine { co ->
            val callback = Choreographer.FrameCallback { frameTimeNanos ->
                co.resumeWith(runCatching { onFrame(frameTimeNanos) })
            }

This article continues for subscribers

Subscribe to Dove Letter for full access to 40+ deep-dive articles about Android and Kotlin development.

Become a Sponsor