Jetpack Compose Mechanisms Quiz: A Complete Solutions Walkthrough
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.)
@Composableis a marker annotation that the Compose Runtime reads via reflection at runtime to decide what to recompose.- The Compose Compiler is a Kotlin compiler plugin that runs at build time, rewriting
@Composablefunctions to inject parameters such as$composerand$changed.- The Compose UI layer is mandatory for the runtime to function; the runtime cannot manage any tree of nodes without
androidx.compose.ui.- Because the runtime is decoupled from Android, the same runtime can drive non Android trees through a custom
Applier.- The
Recomposerthat 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:
@Composableenforcement 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, andUiApplieris only one implementation, soandroidx.compose.uiis not required to manage a tree.
Question 2: Why a normal function cannot call a composable
A regular Kotlin function cannot call a
@Composablefunction, even when their signatures look identical. What is the underlying reason the compiler rejects the call?
- Because the Compose Compiler gives
@Composablefunctions a distinct type with an implicit$composerparameter and calling convention, so calls are only valid where a Composer is in scope.- Because
@Composablefunctions are suspend functions under the hood, and calling one requires a coroutine scope.- Because
@Composablefunctions must be declared inside an Activity, Fragment, or ViewModel.- Because the annotation makes the runtime throw an exception at runtime when no active composition is present.
- Because the Kotlin compiler treats
@Composableas 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:
@Composableis notsuspend. It requires no coroutine and threads aComposer, not a continuation. Thesuspendcomparison 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:
@Composableis 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.)
- Replacing
List<String>with Kotlin'sMutableList<String>would makeUserstable, since the type now explicitly declares its mutability.- Because
List<String>is an interface whose concrete implementation could be mutable, the compiler infersUseras unstable.- Stability is decided per property: adding
@Immutabletoname: Stringis what makesUserskippable.- With strong skipping mode (on by default in the Compose compiler since Kotlin 2.0.20), a composable taking the unstable
Usercan still be skipped when the sameUserinstance is passed again, compared by referential equality.- Annotating
Userwith@Immutablemakes the compiler treat it as stable, a promise it trusts without further checking theListfield.
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