Recomposition 101: How the Compose Compiler Plugin Tracks State Changes

 Unlocking the mysteries of the Slot Table, positional memoization, and the hidden bytecode that powers your UI.

Recomposition 101: How the Compose Compiler Plugin Tracks State Changes

If you’ve ever built a UI with Jetpack Compose, you’ve witnessed the “magic” of Recomposition. You change a MutableState value, and suddenly, the UI updates only the specific parts that need to change.

But how does the runtime know exactly which functions to restart? Standard Kotlin doesn’t have a way to “track” which function reads which variable. This isn’t a feature of the language; it’s a feat of the Compose Compiler Plugin.

Today, we are opening the hood to look at code skipping, “Gap Buffers,” and the positional memoization that makes Compose so efficient.

1. The Hidden Parameters: $composer and $changed

When the Compose compiler sees a function marked with @Composable, it performs a bytecode-level signature rewrite. It injects hidden parameters into your function that manage the lifecycle of the UI.

Your Source Code:

@Composable
fun WelcomeScreen(name: String) {
Text("Hello $name")
}

What the Compiler produces (Conceptual Bytecode):

fun WelcomeScreen(name: String, $composer: Composer, $changed: Int) {
// 1. Start a group in the Slot Table
$composer.startRestartGroup(12345)

// 2. Logic to handle default parameters ($default bitmask)

// 3. UI logic with injected composer
Text("Hello $name", $composer, ...)

// 4. End the group and return a 'RecomposeScope'
$composer.endRestartGroup()?.updateScope { nextComposer ->
// The function 'remembers' how to call itself for re-execution
WelcomeScreen(name, nextComposer, $changed or 1)
}
}

Note: For functions with many parameters, the compiler may generate multiple $changed integers to track every argument's state.

2. The Slot Table and Positional Memoization

Compose doesn’t use a Virtual DOM. Instead, it uses a Slot Table, a sophisticated data structure similar to a Gap Buffer. Think of it as a giant, flat array that records everything about your Composables in the exact order they are called.

When your code runs, the $composer is "writing" into this table. This is why structural stability is vital. If you use a conditional branch (if/else) that adds or removes composables, the Slot Table "shifts." This is why conditionals must be stable or use key() to help the compiler identify elements that have moved rather than disappeared.

Positional Memoization: When you use remember { ... }, Compose doesn't just store a value; it stores it at a specific "address" in the Slot Table. The "identity" of your state is tied to where it was called in the execution flow.

3. State Tracking: The “Scope” Registration

How does changing count.value++ trigger a specific function?

  • The Read: When a Composable reads a State object, the Snapshot system notifies the current $composer.
  • The Subscription: The Composer then registers the current Recompose Scope (the code block being executed) as a “listener” for that specific state.
  • The Invalidation: When the State value changes, the Snapshot system looks up all registered scopes and marks them as invalid.

Crucially, Recomposition is scope-based, not tree-based. Changing a state variable only invalidates the nearest scope that read it, preventing a full-tree “cascade” recomposition.

4. Smart Skipping: The $changed Bitmask

One of the biggest performance wins in Compose is Skipping. If the inputs to a function haven’t changed, Compose skips the function body entirely.

The compiler generates a bitmask (the $changed parameter) to track parameter changes.

  • Stable Types: If a type is “Stable” (like String or an @Immutable class), the compiler can compare it. If the value is the same as the one in the Slot Table, Compose skips the function execution.
  • Unstable Types: If you pass an unstable type (like a standard List), the compiler cannot prove the value hasn't changed internally. This may force a recomposition because the compiler cannot safely skip.

Important: Skipping avoids re-executing the function body, but it does not automatically skip the Layout or Draw phases if those specific phases were invalidated elsewhere.

5. Summary of the Pipeline

  1. Transformation: Compiler injects $composer$changed, and $default bits.
  2. Registration: During execution, the function “subscribes” its scope to any State it reads.
  3. Invalidation: A State change marks the associated Recompose Scope as “dirty.”
  4. Re-execution: On the next frame, the Composer restarts only the “dirty” scopes, using the Slot Table to “remember” old values and the bitmask to skip what it can.

🙋‍♂️ Frequently Asked Questions (FAQs)

Does every State change cause an immediate Recomposition?

No. Recomposition is coordinated with the display’s refresh rate (frame coalescing). If you update a state 100 times between frames, Compose only recomposes once for the final value.

Why is remember necessary?

Composables are just functions. Without remember, your local variables would be re-initialized every time the function restarts. remember tells the $composer to store and retrieve the value from the Slot Table.

Can I see the Slot Table?

While you can’t view the raw table at runtime, the Layout Inspector in Android Studio effectively walks this data structure to show you exactly which Composables are active and what state they hold.

💬 Join the Conversation!

  • Have you ever had a “Recomposition loop”? (Usually caused by modifying state during the execution of a function!)
  • Do you use the Compose Compiler Metrics to find “unstable” classes in your project?
  • Which approach do you prefer: the “Hidden Parameter” transformation of Compose or the “Hooks” approach of React?

📘 Master Your Next Technical Interview

Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend “Mastering Data Structures & Algorithms in Java”. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.


Comments

Popular posts from this blog

No More _state + state: Simplifying ViewModels with Kotlin 2.3

Why You Should Stop Passing ViewModels Around Your Compose UI Tree 🚫

Is Jetpack Compose Making Your APK Fatter? (And How to Fix It)