Jetpack Compose

    Jetpack Compose Slot Table

    A
    AzeemJul 7, 2026 · 9 min read
    57

    If you've ever asked yourself "how does Compose know what to redraw when state changes?" — the answer is a data structure called the Slot Table. It sounds scary, but the idea behind it is actually really simple once you strip away the jargon.

    Let's build it up from scratch.

    The Problem Compose Had to Solve

    Old-school Android Views work by comparing two trees every time something changes — the old view tree and the new one — and figuring out the differences. This is called diffing, and it's expensive.

    Compose said: "What if we never diffed anything? What if we just remembered what we did last time, and only redo the parts that actually changed?"

    That "memory" is the Slot Table.

    Think of It Like a Notebook, Not a Tree

    This is the part that trips people up. You'd expect Compose to store your UI as a tree (because that's how it looks on screen — a parent with children). But it doesn't.

    Instead, Compose writes everything down in the order it happens, like a diary entry:

    @Composable
    fun Profile(name: String) {
        Text("Hello")
        Text(name)
    }
    
    code

    Compose doesn't see "a Profile box containing two Text boxes." It sees a sequence of events:

    1. Entered Profile

    2. Called Text("Hello")

    3. Called Text(name)

    4. Exited Profile

    That's exactly what gets written into the slot table — one line after another, like a notebook, not a tree.

    What's Actually Sitting in a "Slot"?

    A slot is just one entry in that notebook. It can hold:

    • A value from remember { }

    • A piece of state

    • A reference to a UI node

    • Info about which composable was called

    And a group is the boundary marker around a composable call — a "start" and "end" bookmark so Compose knows where one function's stuff begins and ends.

    So really:

    Slot table = a flat list of entries, with start/end bookmarks (groups) showing which entries belong to which composable.

    A Bigger Example

    @Composable
    fun Counter(count: Int) {
        val doubled = remember { count * 2 }
        Text("Count: $count")
        Text("Doubled: $doubled")
    }
    
    kotlin

    Here's roughly what gets written into the notebook the first time this runs:

    Position 0 → START group: Counter
    Position 1 → slot: remember (doubled)
    Position 2 → START group: Text("Count")
    Position 3 → END group
    Position 4 → START group: Text("Doubled")
    Position 5 → END group
    Position 6 → END group: Counter
    
    code

    Notice: it reads top to bottom, like a script — not like a tree diagram. The "tree shape" only exists visually on your screen; underneath, it's one long, ordered list.

    So What Happens When count Changes?

    This is the payoff. When count changes, Compose does not throw away the notebook and start over. Instead:

    1. It marks Counter as "dirty" (needs a re-check).

    2. It re-runs the Counter function.

    3. It walks through the same notebook positions in the same order.

    4. At each position, it asks: "Do I already have something here? Can I reuse it, or does it need updating?"

    For remember { count * 2 } specifically:

    • Compose looks at position 1.

    • Sees there's already a value there.

    • Reuses it — unless the thing being remembered depends on something that changed.

    No rebuilding. No comparing two trees. Just walking a list and reusing what hasn't changed. That's why Compose is fast.

    Two Different Things People Mix Up

    • Invalidation = "this part might have changed, flag it."

    • Recomposition = "actually re-run the code for the flagged part."

    The slot table is what lets Compose separate these two steps — it knows exactly which notebook entries are flagged, so it only re-runs those, not everything.

    Where It Gets Dangerous: Changing Order

    Here's the classic mistake:

    if (showDetails) {
        Text("Details")
    }
    Text("Footer")
    
    kotlin

    When showDetails is true, the notebook looks like:

    0 → START
    1 → Text("Details")
    2 → Text("Footer")
    3 → END
    
    code

    When showDetails is false, the notebook looks like:

    0 → START
    1 → Text("Footer")
    2 → END
    
    code

    See the problem? Text("Footer") used to live at position 2. Now it lives at position 1. As far as the slot table is concerned, a completely different composable now sits where "Footer" used to be. Compose can't just reuse things — it has to figure out the structural change, which is more expensive and can even cause state to reset unexpectedly.

    This is exactly why key { } and movableContentOf exist — they let you tag content with a stable identity so Compose can track it correctly even when its position in the notebook shifts around.

    Common Misconceptions, Cleared Up

    => "The slot table is basically the UI tree." No — it's a recording of what got called and in what order, not a tree structure.

    => "remember stores things globally, like a global variable." No — it stores the value at one specific notebook position. Move that call around, and it may not find its old value anymore.

    => "Recomposition means everything re-runs." No — only the parts that got marked "dirty" re-run. Everything else is skipped.

    Why Should You Actually Care About This?

    Once this clicks, a bunch of "senior-level" Compose questions stop being magic:

    • Why does my state randomly reset when I reorder composables? → Slot position changed.

    • Why does key {} fix my list item bugs? → It gives items a stable identity independent of position.

    • Why is Compose faster than the old View system for frequent updates? → No tree diffing, just notebook position lookups.

    One More Example — Let's Trace It Fully

    Let's use something closer to a real screen, so you can see the whole lifecycle: slot table creation → what's stored → what happens when state changes.

    @Composable
    fun CartScreen(itemCount: Int) {
        val isEligibleForFreeDelivery = remember(itemCount) { itemCount >= 3 }
    
        Text("Items in cart: $itemCount")
    
        if (isEligibleForFreeDelivery) {
            Text("You've unlocked free delivery! 🎉")
        }
    
        Text("Proceed to checkout")
    }
    
    kotlin

    Think of a Swiggy/Zomato-style cart screen: it shows the item count, a free-delivery banner if you've crossed the threshold, and a checkout line.

    Step 1: First Composition — How the Slot Table Gets Built

    The very first time CartScreen runs, Compose executes the function top to bottom and writes one notebook entry per thing it encounters. Let's say itemCount = 3:

    Position 0 → START group: CartScreen
    Position 1 → slot: remember(itemCount) → isEligibleForFreeDelivery = true
    Position 2 → START group: Text("Items in cart: 3")
    Position 3 → END group
    Position 4 → START group: if-block (isEligibleForFreeDelivery)
    Position 5 →   START group: Text("free delivery unlocked")
    Position 6 →   END group
    Position 7 → END group: if-block
    Position 8 → START group: Text("Proceed to checkout")
    Position 9 → END group
    Position 10 → END group: CartScreen
    
    code

    A few things worth noticing about how this table got built:

    • Compose didn't "design" this layout upfront. It literally just executed your function line by line, and each line created one more notebook entry, in order.

    • The remember(itemCount) { ... } call got its own slot (position 1) tied to the key itemCount. Compose also silently stores the key (3) next to it, so it can later check "did this key change?"

    • The if block itself becomes a group (position 4–7) — even conditionals get a group, because Compose needs a boundary to know "this chunk may or may not exist depending on a condition."

    Step 2: Recomposition — itemCount Changes from 3 to 5

    Now say the user adds two more items, so itemCount becomes 5. Compose does not throw away the table and rebuild from scratch. Here's the walk:

    1. Invalidation: Because CartScreen reads itemCount (an observed state), the group for CartScreen (position 0–10) gets marked dirty.

    2. Re-execution starts: Compose re-runs the CartScreen function body.

    3. Position 1 check (remember): Compose sees the key itemCount used to be 3, now it's 5. Since the key changed, the remembered lambda re-runs: isEligibleForFreeDelivery is recalculated → still true (5 ≥ 3). The old value at position 1 gets replaced.

    4. Position 2–3 (Text("Items in cart...")): This group's input (itemCount) changed, so this Text recomposes with the new string.

    5. Position 4–7 (the if block): Since isEligibleForFreeDelivery was true before and is still true, this group still exists in the same shape — Compose reuses its structure and just checks whether the inner Text needs updating (it doesn't, since the string is hardcoded — so it's skipped entirely).

    6. Position 8–9 (Text("Proceed to checkout")): Nothing this composable depends on has changed, so Compose skips re-running it altogether.

    End result: out of everything in CartScreen, only the remember value and the item-count Text actually did any real work. Everything else was walked past and reused as-is.

    Step 3: The Case Where It Gets Expensive — itemCount Drops to 2

    Now say the count drops from 5 to 2. This time isEligibleForFreeDelivery flips from true to false. Here's where it's a bit more costly:

    1. Position 1 (remember) recalculates: key changed (5 → 2), so the lambda re-runs → now false.

    2. Position 2–3 recomposes with the new count text, same as before.

    3. Position 4–7 (the if block): last time, this group existed with a Text inside it. This time, the condition is false, so the group's content needs to be removed entirely — the "free delivery unlocked" Text and its slots get torn down, not just skipped.

    4. Position 8–9 (Text("Proceed to checkout")) is unaffected either way, so it's skipped.

    This is the more expensive path: instead of just reusing or lightly updating a slot, Compose has to actually add/remove groups from the table, because the shape of what got called changed, not just the values inside it.

    Compose UI = a sequence of function calls. Slot table = the notebook that remembers those calls. Recomposition = re-reading the notebook and only rewriting the pages that changed.

    That's the whole idea.

    android
    jetpack compose
    kotlin
    Was this helpful?

    Comments (0)

    No comments yet. Be the first to share your thoughts!

    Leave a Comment

    Get the latest updates

    Join our newsletter and never miss an article.

    By subscribing, you agree to our Privacy Policy and consent to receive updates.

    Ready to Start Learning?

    Join thousands of students building their dream careers

    Explore Courses