Jetpack Compose Mechanisms Quiz: A Complete Solutions Walkthrough

skydovesJaewoong Eum (skydoves)||32 min read

Jetpack Compose Mechanisms Quiz: A Complete Solutions Walkthrough

The Jetpack Compose Mechanisms Quiz asks ten questions that look like everyday Compose code: a parameter that refuses to skip, an effect that reads a stale value, a modifier chain that lays out the wrong way. Each one has a single defensible answer once you know what the runtime is doing underneath. Most developers can pick the right answer to a few of them by instinct, but the deeper question is why each answer is correct, and what the compiler, the runtime, and the UI layer are actually doing when you hit these cases in production.

In this article, you'll dive deep into the reasoning behind all ten questions, exploring how the Compose compiler rewrites your functions, why the runtime has no idea Android exists, how positional memoization decides what survives a recomposition, why collectAsState keeps running in the background, how derivedStateOf filters invalidations, and why modifier order changes your layout. Every answer is traced to the actual source in androidx.compose.runtime, androidx.compose.ui, androidx.compose.foundation, and androidx.lifecycle.

You can take the quiz first at doveletter.dev/quiz/compose. For each question below, you'll see the options, the correct answer, the mechanism that makes it correct, and why the other options miss. Each answer also points to the chapter and section of the Jetpack Compose Mechanisms book where that mechanism is covered in full, in case you want to read further.

Architecture

Question 1: The three layers and what depends on what

Jetpack Compose is commonly described as three layers: the Compose Compiler, the Compose Runtime, and the Compose UI. Which of the following statements about how these layers relate are correct? (Select all that apply.)

  1. @Composable is a marker annotation that the Compose Runtime reads via reflection at runtime to decide what to recompose.
  2. The Compose Compiler is a Kotlin compiler plugin that runs at build time, rewriting @Composable functions to inject parameters such as $composer and $changed.
  3. The Compose UI layer is mandatory for the runtime to function; the runtime cannot manage any tree of nodes without androidx.compose.ui.
  4. Because the runtime is decoupled from Android, the same runtime can drive non Android trees through a custom Applier.
  5. The Recomposer that schedules recomposition in response to snapshot state changes is part of the runtime, not the Android framework.

The correct answers are 1, 3, and 4. The wrong statements are options 0 and 2.

The three names describe three separate concerns, and the dependency between them points in one direction only. The Compose Compiler is a Kotlin compiler plugin that runs during your build. The Compose Runtime, which lives in androidx.compose.runtime, owns the slot table (its in memory record of the composition), the snapshot state system, recomposition, the Recomposer, and the Applier. The Compose UI layer, in androidx.compose.ui, owns LayoutNode, measurement, and drawing. The UI layer depends on the runtime. The runtime does not depend on the UI layer.

Option 1 describes the compiler's job. It rewrites the signature of every composable. A function you write as Greeting(name: String) leaves the compiler as Greeting(name: String, $composer: Composer?, $changed: Int), wrapped in a restart group with a skip check. The $changed parameter is a bitmask of flags that tells the runtime which arguments might have changed, and the restart group is the unit the runtime can re-execute on its own, a point Question 4 returns to. The runtime is built around that contract. If you look at the Composer interface, its own documentation states the relationship:

/**
 * Composer is the interface that is targeted by the Compose Kotlin compiler plugin and used by code
 * generation helpers. It is highly recommended that direct calls these be avoided as the runtime
 * assumes that the calls are generated by the compiler ...
 */
public sealed interface Composer

This is why option 0 is wrong. There is no reflection involved. The decision about what to recompose is encoded at build time into the $changed bitmask and the restart group, not discovered at runtime by inspecting annotations.

Options 3 and 4 are two consequences of the same design. The runtime never touches LayoutNode directly. It talks to an Applier, an interface that abstracts away what kind of tree is being built. Looking at the core operations the Applier interface defines:

public interface Applier<N> {
    public val current: N
    public fun down(node: N)
    public fun up()
    public fun insertTopDown(index: Int, instance: N)
    public fun insertBottomUp(index: Int, instance: N)
    public fun remove(index: Int, count: Int)
    public fun move(from: Int, to: Int, count: Int)
    public fun clear()
}

The runtime emits these operations against an Applier<N> and has no idea what N is. The UI layer plugs in UiApplier, which builds a tree of LayoutNode. Vector graphics plug in VectorApplier, which builds a tree of VNode. Tests plug in plain node trees. That is the whole point of the abstraction, and it is exactly why option 2 is false. The runtime does not need androidx.compose.ui to manage a tree. The UI layer is just one Applier among several.

Option 4 places the Recomposer correctly. It is declared in androidx.compose.runtime:

public class Recomposer(effectCoroutineContext: CoroutineContext) : CompositionContext()

The constructor takes a plain CoroutineContext, not anything Android specific. The Recomposer reacts to snapshot state invalidations and schedules recomposition. On Android it is hosted by a window and fed frame timing by the platform Choreographer, but the class itself is runtime code with no Android dependency baked in.

Why the other options miss:

  • Option 0: @Composable enforcement and skip decisions are resolved by the compiler at build time, not by runtime reflection over annotations.
  • Option 2: the runtime manages trees through the Applier<N> abstraction, and UiApplier is only one implementation, so androidx.compose.ui is not required to manage a tree.

Question 2: Why a normal function cannot call a composable

A regular Kotlin function cannot call a @Composable function, even when their signatures look identical. What is the underlying reason the compiler rejects the call?

  1. Because the Compose Compiler gives @Composable functions a distinct type with an implicit $composer parameter and calling convention, so calls are only valid where a Composer is in scope.
  2. Because @Composable functions are suspend functions under the hood, and calling one requires a coroutine scope.
  3. Because @Composable functions must be declared inside an Activity, Fragment, or ViewModel.
  4. Because the annotation makes the runtime throw an exception at runtime when no active composition is present.
  5. Because the Kotlin compiler treats @Composable as an inline only modifier that cannot cross function boundaries.

The correct answer is option 0.

@Composable is not a decoration on an ordinary function. It changes the function's type. The annotation's own documentation describes the mental model:

/**
 * Annotating a function or expression with [Composable] changes the type of that function or
 * expression. For example, [Composable] functions can only ever be called from within another
 * [Composable] function. A useful mental model for [Composable] functions is that an implicit
 * "composable context" is passed into a [Composable] function ...
 */
public annotation class Composable

That implicit context is the $composer parameter the compiler threads through every call. A composable function receives a Composer and passes it into each composable it calls. A plain Kotlin function has no Composer to pass, so the call site is rejected. This is a static type check performed by the compiler's frontend, the same kind of check that stops you from passing a String where an Int is expected.

A useful way to think about it is function coloring. The suspend keyword colors a function: a suspend function can only be called from another suspend function or a coroutine, because it carries an implicit continuation. @Composable colors a function in the same structural way, except the implicit parameter it carries is a Composer rather than a continuation. The coloring is the mechanism that decides where the function can and cannot be called.

Why the other options miss:

  • Option 1: @Composable is not suspend. It requires no coroutine and threads a Composer, not a continuation. The suspend comparison is an analogy for coloring, not the actual mechanism.
  • Option 2: the only requirement is that the call sits inside another composable, which has nothing to do with Android host classes.
  • Option 3: the call never compiles, so no runtime exception is involved. Enforcement is a build time check.
  • Option 4: @Composable is an annotation, not an inlining modifier, and composables are not required to be inline.

The compiler

Question 3: Stability, List, and strong skipping

A composable takes a parameter of type data class User(val name: String, val tags: List<String>). Which statements about its stability and recomposition skipping are correct? (Select all that apply.)

  1. Replacing List<String> with Kotlin's MutableList<String> would make User stable, since the type now explicitly declares its mutability.
  2. Because List<String> is an interface whose concrete implementation could be mutable, the compiler infers User as unstable.
  3. Stability is decided per property: adding @Immutable to name: String is what makes User skippable.
  4. With strong skipping mode (on by default in the Compose compiler since Kotlin 2.0.20), a composable taking the unstable User can still be skipped when the same User instance is passed again, compared by referential equality.
  5. Annotating User with @Immutable makes the compiler treat it as stable, a promise it trusts without further checking the List field.

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