Interview QuestionPractical QuestionFollow-up Questions

Composable Lifecycle and Recomposition in Jetpack Compose

skydovesJaewoong Eum (skydoves)||13 min read

Composable Lifecycle and Recomposition in Jetpack Compose

Every composable function in Jetpack Compose follows a lifecycle defined by three events: entering the Composition, recomposing zero or more times, and leaving the Composition. Unlike the traditional Android View lifecycle with its many callback methods, this model is simpler in concept but more complex in its runtime implementation. The Compose runtime tracks each composable instance through a slot table, manages identity with positional keys, and uses snapshot state observation to determine exactly which composables need re-execution. Understanding these internals is essential for writing performant Compose code and diagnosing unexpected recomposition behavior. By the end of this lesson, you will be able to:

  • Explain how the Compose runtime tracks composable instances through the slot table during initial composition and recomposition.
  • Describe how positional memoization and call site identity determine composable lifecycle boundaries.
  • Identify how the snapshot state observation system triggers targeted recomposition scopes.
  • Trace how the Recomposer coordinates invalidation, scheduling, and re-execution of composable functions.
  • Apply strategies for controlling recomposition granularity, including stability, keys, and scope boundaries.

The Slot Table and Composable Identity

The Composition stores the state of all composable instances in a data structure called the slot table. During initial composition, the Composer writes entries into this table in execution order. Each composable call site gets a unique identity based on its position in the call hierarchy, known as positional memoization.

The Composer class manages a cursor into the slot table. When a composable function executes, the Composer creates or updates a group in the table:

// Simplified representation of how the Composer tracks groups
@ComposeCompilerApi
fun startRestartGroup(key: Int): Composer {
    startGroup(key)
    // ...
    return this
}

The compiler transforms every composable function to include startRestartGroup() and endRestartGroup() calls. These bracket the function body and define a restart scope, which is the unit of recomposition. The key parameter is a synthetic integer derived from the source location of the composable call. This means two calls to the same composable function from different call sites get different keys and are tracked as separate instances.

When a composable enters the Composition for the first time, the Composer is in insert mode. It allocates new slots, stores parameter values and any remember blocks, and registers the group. On subsequent recompositions, the Composer switches to update mode. It walks through the existing slot table entries, comparing the stored key with the expected key at each position. If the keys match, the composable is the same instance as before and may be updated or skipped. If the keys do not match, the runtime detects a structural change and must insert or remove groups accordingly.

Entering the Composition

A composable enters the Composition when it is called for the first time in a given position. This happens during either the initial composition of the entire tree or when a parent composable introduces a new child, for example through a conditional branch evaluating to true for the first time:

@Composable
fun ProfileScreen(showBadge: Boolean) {
    Column {
        ProfileHeader()
        if (showBadge) {
            // Badge enters the Composition when showBadge becomes true
            AchievementBadge()
        }
    }
}

When AchievementBadge enters the Composition, the Composer inserts a new group into the slot table. Any remember blocks inside AchievementBadge execute and store their initial values. Any LaunchedEffect or DisposableEffect blocks register their effects. The Applier then creates the corresponding layout node and inserts it into the node tree at the correct position.

The Applier is the bridge between the Composition and the UI tree. On Android, the standard Applier is UiApplier, which operates on LayoutNode instances. When a composable enters the Composition, the Applier calls insertTopDown() or insertBottomUp() to place the new node:

internal class UiApplier(root: LayoutNode) : AbstractApplier<LayoutNode>(root) {
    override fun insertTopDown(index: Int, instance: LayoutNode) {
        current.insertAt(index, instance)
    }

    override fun insertBottomUp(index: Int, instance: LayoutNode) {
        // Intentionally empty: UiApplier is a top-down applier
    }

    override fun remove(index: Int, count: Int) {
        current.removeAt(index, count)
    }
}

This interview continues for subscribers

Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.

Become a Sponsor