Jetpack Compose Mechanisms: A Guide to the Series, the Talk, and the Book

skydovesJaewoong Eum (skydoves)||16 min read

Jetpack Compose Mechanisms: A Guide to the Series, the Talk, and the Book

Over the past few months, Jetpack Compose Mechanisms has taken several forms: a book with a companion course, a talk at DroidKaigi 2026 and Droidcon Berlin 2026, the slides behind it, and a three part article series here on Dove Letter. They all chase the same question, what actually runs beneath a @Composable function, but from different distances. The talk gives you the shape of the answer in about 40 minutes, the series reads the source behind each claim, and the book goes everywhere the series could not.

In this article, you'll find all of it in one place: the idea that connects the three layers of Compose, a summary of each part of the series and the one thing to take from it, what the talk and the slides cover and where the articles go further, what the book and the course add, and a few reading paths depending on how deep you want to go.

The big picture: Three layers and one idea

Every piece of this project follows the same map. Compose is usually introduced as a UI toolkit, but it has three parts, and only two of them are libraries.

Composable functions pass through the Compose compiler, the Compose runtime, and Compose UI before they become pixels

  1. The Compose compiler is a Kotlin compiler plugin that rewrites your composable functions at build time.
  2. The Compose runtime executes the rewritten functions and keeps a record of the composition in memory: what was called, in what order, and which values were remembered along the way.
  3. Compose UI turns that record into a tree of LayoutNodes, measures and places them, and draws the result into pixels.

One idea ties the three parts of the series together: Compose does as little work as it can prove is safe, and the proof is always a comparison. Inside a composable, the compiler generates the comparison and the runtime stores the values it compares against. Your code decides how often those comparisons succeed, and sometimes the comparison that matters happens upstream of the composable that recomposes, in a StateFlow or a remember key. Most of what follows shows where one of those comparisons happens.

The series at a glance

PartLayerThe question it answers
Part 1: How the Compiler Turns a Function into a ComposableCompilerWhat does @Composable change, and how does a composable decide to skip?
Part 2: How the Runtime Remembers and How the UI Becomes PixelsRuntime and UIWhere do remembered values live, how does a state change reach the screen, and how does the composition become pixels?
Part 3: Recomposition Performance and Why Stability Still MattersPerformanceWhen does Compose do more work than it needs to, and where does the fix belong?

Each part stands on its own, but they are written to be read in order: Part 2 builds on the groups and the changed() check from Part 1, and Part 3 uses both to explain where unnecessary recompositions come from.

Part 1: The compiler and the skip decision

Part 1 starts from a rule you have already hit: a composable can only be called from another composable. That rule has the same root as the one for suspend functions. The Kotlin compiler adds a hidden Continuation parameter to every suspend function, and the Compose compiler adds hidden parameters to every composable:

// What you write
@Composable
fun Greeting(name: String) {
    Text("Hello, $name")
}

// The signature the compiler produces
fun Greeting(name: String, $composer: Composer?, $changed: Int)

$composer is the runtime context, and every composable passes it down to the composables it calls. A regular function has no composer to pass, which is why it cannot call a composable. From there, the article follows the rest of the rewrite:

  • Restart groups: the compiler wraps the body in startRestartGroup and endRestartGroup. When the body reads a state, endRestartGroup hands back a scope that stores a lambda, which lets the runtime call this one function again without running its caller. That is what makes recomposition selective, and it is only safe because composables are expected to be idempotent.
  • A compiler plugin, not an annotation processor: KAPT and KSP can only generate new files next to yours. Compose has to edit the functions you wrote, so it runs inside the K2 compiler, checking your code on FIR, which is where the calling context error comes from, and rewriting it on IR.
  • The skip decision: $changed packs what the caller already knows about each argument into a few bits per parameter: Uncertain, Same, Different, or Static. When the caller does not know, the generated code compares the argument with the value stored on the previous run, and shouldExecute decides whether the body runs or the runtime jumps to the end of the group.

The rule to keep from Part 1: a composable skips only when each argument its body uses matches the one stored on its previous run, by equals() for stable types and by identity for unstable types and lambdas. When a type's stability is only known at runtime, a bit the caller passes picks between the two. Everything in Part 3 builds on that rule.

Part 2: Where values live and how they become pixels

Part 2 starts from four versions of the same composable, each holding a value a different way, and uses them to separate two jobs that are easy to blur into one:

VersionKept across runsReports changes
Dove()NoNo
remember { Dove() }YesNo
mutableStateOf(Dove())NoYes, but a new state replaces it on every run
remember { mutableStateOf(Dove()) }YesYes

The runtime half of the article explains each column:

  • remember keeps values in the SlotTable. It is a call to Composer.cache, which reads the value stored at the same position on the previous run and computes a new one only when nothing is stored yet or a key changed. The SlotTable is a gap buffer, the same structure many text editors use, so inserting content in the middle, like an Icon that appears once a count becomes positive, moves a gap instead of rebuilding the table.
  • The position is the identity. The runtime recognizes a value by the group key the compiler generated and by the order in which groups appear. That is why each call site gets its own state, and why content that moves around needs key().
  • Reading a State is the subscription. There is no observe or collect. A read during composition goes through the snapshot system's read observer, which records each scope that reads it, and a later change to the value invalidates exactly those scopes.
  • Writes are coalesced into frames. A write does not recompose anything by itself. The Recomposer waits for the next frame, so several writes made before it cost one recomposition.

The UI half follows the result to the screen. The runtime never looks inside a node; it edits the tree through the Applier, an interface generic over the node type, which is why the same runtime also drives vector graphics, Glance widgets, DOM nodes, and terminal UIs. Compose UI supplies UiApplier for LayoutNode, almost every component ends in the Layout composable with a measure policy and a modifier, and AndroidComposeView, an ordinary ViewGroup, draws the root LayoutNode onto the Canvas that Android hands it. Even Text measures nothing in its own measure policy: the measuring and drawing happen in its modifier node.

The two questions to keep from Part 2: does a value need to survive recomposition? That is the job of remember. Does a change to it need to reach the screen? That is the job of State. Most UI state needs both.

Part 3: Performance, and where new instances come from

Part 3 starts with a number. In Google's hero benchmarks, which compare the View based and Compose based versions of my open source Pokedex app, Compose has had the same scrolling jank rate as Views since Compose 1.9.0. For scrolling, the framework is no longer the bottleneck, and what remains is the work your own code asks for.

The article then scales the skip decision from Part 1 up to a whole tree. A recomposition is more than calling your function: the runtime finds the invalidated scopes, walks the SlotTable to them, runs them again, and applies any changes to the LayoutNode tree. Skipping at the top of a subtree saves everything below it, so everything depends on whether Compose can tell that an input is the same, and that depends on stability:

  • Strong skipping, on by default since Kotlin 2.0.20, made composables with unstable parameters skippable, but only by comparing those parameters by reference, with ===. It did not make any type stable.
  • Kotlin 2.4 no longer treats interfaces such as List as unstable. Their stability is decided at runtime, and because an interface carries no stability information, a List argument is compared with equals(). An unannotated class that the Compose compiler compiled in another file is decided at runtime too, from a field the compiler generates on it. For Part 3's Message model, which keeps a plain List, that field still says unstable even once every property is a val.

Then come three everyday cases, each one moving the source of new instances further away from the composable:

  1. A list mapped inside the composable. users.map { ... } builds a new list on every recomposition. Wrapping it in remember(users) keeps the instance, and in the article's test on Kotlin 2.3.21 the child went from 6 runs to 1. Kotlin 2.4 already skips here by comparing the lists with equals(), and the article explains why the remember is still worth keeping.
  2. A ViewModel that copies its state. This one costs no recomposition. StateFlow drops a new value that equals the current one, so a data class copy with the same contents never reaches Compose.
  3. A repository that remaps every row. Marking one of 12 visible messages as read recomposed all 12 bubbles, because every row arrived as a new instance of an unstable Message. Making the model stable, with val properties and an ImmutableList, brought it down to 1.

Part 3 also covers @Immutable, the stability configuration file that Now in Android uses for its whole model package, how to read the Compose compiler reports, and Compose Stability Analyzer for seeing stability right in the editor.

The habit to keep from Part 3: when a composable recomposes more often than its data changes, follow its arguments back to the place that created them and fix the problem there. That place is often somewhere other than the composable that recomposes.

The talk: Jetpack Compose Mechanisms

The series is based on my talk Jetpack Compose Mechanisms, given at DroidKaigi 2026 and Droidcon Berlin 2026, and both sessions were recorded:

The talk follows the same map in one sitting. It opens with a short history of Compose and its three layer structure, then walks through the composable function and the compiler, the runtime with remember, the SlotTable, and State, the UI layer from the Applier to AndroidComposeView, and finally performance and stability. If you want the whole picture before you read anything, start here.

The slides: Every diagram in one deck

The slides are on Speaker Deck, 86 of them, covering the composable function, the Compose compiler, the Compose runtime, Compose UI, and Compose performance. The diagrams throughout the series come from this deck, so it also works as a visual index: the composable rewrite, the gap moving through the SlotTable, and the Applier between the runtime and the tree are all there.

The slides also include one measurement that the articles do not. For Now in Android's For You feed screen, with strong skipping on, the slide reports 140 recompositions with an unstable UserNewsResource model and 30 with a stable one. It pairs that measurement with the stability configuration file Now in Android uses for its model package, the same file Part 3 walks through.

Where the articles go further than the talk

A talk has to simplify, and writing the series meant checking every slide against the current sources. A few things changed on the way:

  • The skip check: the slides show the inline $dirty and 0b0011 != 0b0010 || !$composer.skipping form. Since Kotlin 2.2 turned the compiler's PausableComposition flag on by default, the compiler calls shouldExecute instead, so the runtime can make the pausing decision. Part 1 reads the current form and says when the inline one still appears.
  • remember { 0 }: the slides show var count = remember { 0 } keeping its count across a recomposition. It does not, because remember stored the value 0, not the variable. Part 2 shows what happens instead and what to remember.
  • Kotlin 2.4: the slides show a List parameter reported as unstable. Kotlin 2.4 changed that verdict to one decided at runtime, and Part 3 measures where that helps and where it does not.
  • Measured numbers: the recomposition counts in Parts 2 and 3 come from tests that actually ran on the JVM, with the setup stated, rather than from reading the code alone.

The book: Jetpack Compose Mechanisms

Each article in the series ends by pointing to the book, and this is where it fits in. Jetpack Compose Mechanisms follows Compose through the same layers in four chapters, the Compose compiler, the Compose runtime, Compose UI, and performance, across 539 pages. Like the series, it is read from the AOSP source, and it is updated for Kotlin 2.4.0 and Compose Compiler 2.4.0.

The series takes one path through each layer. The book covers the territory around that path:

  • The compiler: stability inference, lambda memoization, durable function keys, the IR transformation pipeline traced through every pass, compiler reports, and Live Literals.
  • The runtime: the snapshot system and its MVCC isolation, derived state, effects, CompositionLocal, the Recomposer, MovableContent, and bridging state with Flow.
  • The UI: modifier order, the Modifier.Node system and the coordinator chain, single pass measurement, the rendering pipeline, input, focus, semantics, lookahead, SubcomposeLayout, shared element transitions, nested scrolling, and lazy layouts, ending with a miniature Compose UI built from scratch.
  • Performance: the stability inference algorithm phase by phase, state read placement, lambda recomposition, measurement from compiler reports to CI, six anti patterns that quietly slow Compose down, and a full case study that optimizes a janky chat screen recomposing 47 times a second.

It is available on Leanpub as PDF, EPUB, and web editions, with Korean and Japanese translations. howcomposeworks.com lists where to get each format, including the print editions.

The course: Practice on top of the book

The Jetpack Compose Mechanisms course turns the same material into a structured path of 8 chapters and 59 lessons, with 240+ interactive assessments, practical questions with full answers, and a certificate when you finish. It is built on the book but is not a copy of it: it has roughly 1.5 times more content, with expanded walkthroughs and follow up challenges, and the full answers to its practical questions are not in the book edition.

If you would like to check where you stand first, the Jetpack Compose Mechanisms Quiz asks ten practical questions that quietly probe Compose internals, and its solutions walkthrough explains the mechanism behind each answer.

Where to start: Three paths

Depending on how much time you have:

  • About 40 minutes: watch either recording of the talk with the slides open. You will come away with the three layers, the skip decision, and the stability cases.
  • An evening: read Part 1, Part 2, and Part 3 in order. Each one reads the source behind the claims in the talk, and Parts 2 and 3 run the examples to measure them.
  • All of it: read the book, then work through the course to make it stick.

When one mechanism catches your attention, these Dove Letter articles go deeper into a single topic:

Conclusion

In practice, you do not need every detail here to write good Compose code, but each layer answers a different kind of question you will eventually ask. When a composable cannot be called, or refuses to skip, the answer is in what the compiler does to it. When a value resets or the screen does not update, it is in the SlotTable and the snapshot system. When a list recomposes every row, it is in where your code creates new instances. Knowing which layer to look in is most of the work.

The talk, the slides, the series, and the book cover the same subject at different depths, and they all point at the idea the series ends on: Compose does as little work as it can prove is safe, and the proof is always a comparison. Once you can see where each comparison happens, recomposition stops being something that happens to you and becomes something you can trace.

As always, happy coding!

— Jaewoong (skydoves)