Compose recomposition is the mechanism that re-executes composable functions when their inputs change. Understanding how Compose decides what to recompose, and when it decides not to, is essential for writing efficient UIs.
Snapshot State: The Engine
Compose's state system is built on a snapshot system. When you write val count by remember { mutableStateOf(0) }, you're creating a State<Int> that lives inside Compose's snapshot graph.
// These are equivalent — both create snapshot-backed state
val count1 = remember { mutableStateOf(0) }
val count2 by remember { mutableStateOf(0) } // delegate syntax
// Multiple state values — use mutableStateListOf / mutableStateMapOf
val items = remember { mutableStateListOf<String>() }
val map = remember { mutableStateMapOf<String, Int>() }
When any State read during a composable's execution changes, Compose schedules that composable for recomposition.
Reading State = Subscribing to Changes
// ✅ State read INSIDE the composable — Compose knows to recompose this scope
@Composable
fun Counter(count: State<Int>) {
Text("Count: ${count.value}") // reading count.value HERE = subscription
}
// ❌ State read OUTSIDE the composable — Compose can't track this
@Composable
fun Counter(countValue: Int) { // int passed by value — no reactivity
Text("Count: $countValue")
}
// ✅ Correct: pass State object or use ViewModel
@Composable
fun Counter(viewModel: CounterViewModel = viewModel()) {
val count by viewModel.count.collectAsStateWithLifecycle()
Text("Count: $count")
}
What Triggers Recomposition
- State read changes:
mutableStateOf,mutableStateListOf,StateFlow.collectAsState() - Composable inputs change: parameters that are not
@Stableor primitive change value CompositionLocalchanges:LocalContext, customCompositionLocalProvider
What does NOT trigger recomposition:
- Parameters that are equal to their previous values (for
@Stableor primitive types) - State that is read in a lambda/modifier that Compose defers to a later phase
@Stable and @Immutable
Compose uses these annotations to skip recomposition when all parameters are equal:
// Without @Stable: Compose can't guarantee equality → recomposes every time parent does
data class ArticleState(
val title: String,
val body: String,
val isBookmarked: Boolean
)
// With @Stable: Compose knows to skip if none of the visible properties changed
@Stable
data class ArticleState(
val title: String,
val body: String,
val isBookmarked: Boolean
)
// @Immutable: even stronger — Compose can skip even without checking properties
// Use only when the object truly never changes after creation
@Immutable
data class Theme(val primaryColor: Color, val fontFamily: FontFamily)
Avoiding Unnecessary Recomposition
// ❌ Lambda capture forces recomposition of the parent when count changes
@Composable
fun Parent(count: Int, onClick: () -> Unit) {
// Child recomposes because onClick is a new instance each recomposition
Child(label = "Click me ($count)", onClick = onClick)
}
// ✅ Remember the lambda to stabilize it
@Composable
fun Parent(count: Int, onCount: () -> Unit) {
val stableOnClick = rememberUpdatedState(onCount)
Child(
label = "Click me ($count)",
onClick = { stableOnClick.value() } // stable lambda that wraps the latest callback
)
}
DerivedStateOf: Computed Values
Use derivedStateOf when a value is computed from other state but changes less often:
@Composable
fun ListWithScrollButton(items: List<String>) {
val listState = rememberLazyListState()
// ✅ derivedStateOf: only triggers recomposition when the boolean changes,
// not on every scroll pixel
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 2 }
}
Box {
LazyColumn(state = listState) { /* items */ }
if (showScrollToTop) {
FloatingActionButton(onClick = { /* scroll to top */ }) {
Icon(Icons.Default.ArrowUpward, "Scroll to top")
}
}
}
}
Key Takeaways
| Concept | Rule |
|---|---|
remember | Survives recomposition; use for all mutable state and expensive computations |
mutableStateOf | Creates snapshot-backed state that triggers recomposition on write |
@Stable | Allows Compose to skip recomposition when parameters are equal |
derivedStateOf | Computed values that change less frequently than their sources |
| Lambda stability | Unstable lambdas force recomposition; use rememberUpdatedState or stable references |
| Snapshot scope | State reads inside a composable scope — not in callbacks or coroutines — create subscriptions |