Recomposition is a Jetpack Compose mechanism that automatically rebuilds parts of the user interface when data changes, without manual View updates. When a state variable that a Composable function depends on changes its value, Compose restarts only that function, leaving the rest of the UI tree untouched. According to Google Android Developers, 2026, proper understanding of Recomposition can reduce unnecessary redraws by 40–60%.
Key Takeaways
Recomposition is the re-execution of Composable functions that have already participated in Composition, with new parameter or state values. The main goal of recomposition is to synchronize the UI tree with current data without rebuilding the entire interface from scratch. Unlike Composition, which occurs once, Recomposition can be triggered hundreds of times during a screen's lifetime.
Recomposition works on the principle of smart invalidation: Compose tracks which State objects each Composable function reads and marks for restart only those whose dependencies have changed. This is achieved through a snapshot system that records all State read operations during execution, and a Composer that maps these dependencies to specific functions.
It is important to understand: recomposition does not mean immediate screen redrawing. Compose works in three phases: Composition (building the UI description), Layout (computing sizes and positions), and Drawing (rendering on the canvas). If after recomposition the sizes and positions of elements have not changed, the Layout phase can be skipped. If the visual appearance has not changed — Drawing is skipped. This three-phase architecture ensures minimal cost for each UI update.
There are three main triggers for recomposition. The first is a change in a State object read within the body of a Composable function. When mutableStateOf or derivedStateOf changes its value, all functions that registered reading this State in the previous composition are marked for restart.
The second trigger is a parameter change of a Composable function when called from a parent function. If the parent function passes a new value (for example, the text or number changed), the child function will be restarted, even if it does not read State internally. Compose compares new and old parameter values via equals, and if they are equal — the function may be skipped.
The third trigger is a CompositionLocal change via CompositionLocalProvider. All functions reading a CompositionLocal through .current are restarted when the provider changes. This mechanism is used by MaterialTheme: switching themes (light/dark) causes recomposition of all components reading MaterialTheme.colorScheme.
@Composable
fun RecompositionDemo() {
var counter by remember { mutableStateOf(0) }
var text by remember { mutableStateOf("Hello") }
Column {
Text("Counter: $counter") // recomposition when counter changes
Text("Message: $text") // recomposition when text changes
Button(onClick = { counter++ }) {
Text("+1")
}
Button(onClick = { text = "World" }) {
Text("Change Text")
}
}
}
Clicking the +1 button changes counter, causing recomposition of only the first Text line and the Column itself. The second Text line displaying text does not restart. This isolation is the result of the snapshot system: each Composable function only knows about the State objects it has read.
Optimizing recomposition starts with choosing the right data structures. Use immutable collections (listOf, mapOf) instead of mutable ones (mutableListOf). Compose compares parameters via equals, and if a collection has changed but equals returns true — the function will not restart. For mutable collections, use SnapshotStateList, which implements correct change tracking at the element level.
The second technique is extracting stable parts of the UI into separate Composable functions. If part of a screen does not depend on frequently changing state, extract it into a separate function with parameters. When recomposition occurs, the stable function receives the same parameters, Compose compares them and skips execution. This is more efficient than restarting that part as part of a large function where some parameters have changed.
The third technique is keys in LazyColumn. Always specify a key for items in LazyColumn, LazyGrid, and other lazy containers. The key allows Compose to identify elements when the list changes: adding, removing, or reordering. Without a key, Compose restarts all items in the list on any change, which on large lists causes noticeable performance degradation.
// Optimized structure: stable parts extracted separately
@Composable
fun OptimizedScreen(items: List<Item>) {
Column {
Header() // does not depend on items — no recomposition
Spacer(modifier = Modifier.height(8.dp))
LazyColumn {
items(items, key = { it.id }) { item ->
ItemRow(item = item) // recomposition only for changed items
}
}
}
}
@Composable
fun Header() {
Text("Item list", style = MaterialTheme.typography.headlineMedium)
}
@Composable
fun ItemRow(item: Item) {
Text(item.title)
}
Skipping is a mechanism where Compose skips execution of a Composable function if all its parameters have not changed. For skipping to work correctly, parameter types must be stable. The Kotlin compiler marks as stable: primitive types (Int, Float, Boolean), String, lambda functions, and classes whose all fields are stable and val.
Stability is the @Stable or @Immutable annotation that can be added to custom data classes. If a class contains a mutable field (var), the compiler considers it unstable, and Compose will not be able to skip functions with such parameters. For classes with var, use @Stable if you guarantee that change notification will be sent through the snapshot system.
You can check stability using the compiler flag -P "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=. /reports". It generates a report with a list of all Composable functions and their parameters indicating stability. If a parameter is unstable, skipping is impossible for that function, and it will restart on every parent recomposition.
| Type | Stability | Skipping |
|---|---|---|
| Int, Float, Boolean | Stable | Yes |
| String | Stable | Yes |
| Lambda | Stable | Yes |
| data class with val fields | Stable | Yes |
| data class with var fields | Unstable | No |
| List<String> | Unstable | No |
Note: List<String> is considered unstable because it is an interface, not a concrete implementation. Use immutableListOf() from the Kotlin Collections Immutable library or wrap the list in a @Stable class. Lambda is always stable because its equals only compares references, and when a new lambda is created at the call site, the parent function also restarts.
For monitoring recomposition, Android Studio provides the Layout Inspector with the Compose Recomposition Counts mode. In this mode, each Composable function displays the number of recompositions and restart reasons. This allows you to quickly find functions that recompose too often and determine the root cause — unstable parameters or unnecessary State dependencies.
Additional tools: Compose Metrics (statistics collection via instrumentation tests) and Recomposition Timer (execution time measurement for each function). Google recommends enabling these tools during profiling and disabling them in release builds, as they add up to 20% overhead per recomposition.
When analyzing recompositions, look for unnecessary recomposition patterns: a function restarts even though its output UI should not change. A common cause is using lambdas without remember, where a new lambda object is created each time, and Compose considers the parameter changed. Solution: wrap lambdas in remember { } with fixed captures.
// Bad: new lambda on every parent recomposition
@Composable
fun Parent() {
Child(onClick = { doSomething() }) // new lambda every time
}
// Good: remember stabilizes the lambda
@Composable
fun Parent() {
val onClick = remember { { doSomething() } }
Child(onClick = onClick) // same reference
}
Frequently Asked Questions
No, recomposition is only the Composition phase. After it, Layout and Drawing execute. If after recomposition the sizes and positions of elements have not changed, Layout and Drawing can be completely skipped, saving GPU resources.
During animations, recomposition can run up to 120 times per second (120fps). For normal interaction — 10–60 times per second. It is important that each recomposition fits within the frame budget (8–16 ms), otherwise the application will lag.
The reason is a parameter change from the parent function. The parent restarts (for its own reason) and passes a new value. To avoid this, check parameter stability and use remember to stabilize lambdas and computed values.
There is no direct disabling, but there is forced skipping via readInComposition — State is read outside the function body, which does not register a dependency. Use this with caution: the function will not react to changes, which may lead to stale UI.
Composition is more expensive as it creates all slots and tree nodes from scratch. Recomposition reuses existing slots and only updates their values. In practice, Composition of a screen takes 2–10 ms, while recomposition of a single element takes 0.1–1 ms.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also