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.
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
$changedintegers 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
Stateobject, 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
Statevalue 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
Stringor an@Immutableclass), 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
- Transformation: Compiler injects
$composer,$changed, and$defaultbits. - Registration: During execution, the function “subscribes” its scope to any State it reads.
- Invalidation: A State change marks the associated Recompose Scope as “dirty.”
- 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.
- E-book (Best Value! 🚀): $1.99 on Google Play
- Kindle Edition: $3.49 on Amazon
- Also available in Paperback & Hardcover.

Comments
Post a Comment