mutableStateOf and Recomposition
mutableStateOf(initialValue) creates a MutableState<T> object — an observable holder that Compose's runtime can subscribe to. When you read a State's .value inside a composable function during composition, Compose records that composable as a 'subscriber' of that state object. When the value later changes, Compose looks up every subscriber that read it and schedules those composables (and only those) for recomposition. This fine-grained, read-tracking mechanism is the core reason Compose can be efficient: it doesn't recompose your whole screen on every state change, only the specific composable functions that actually read the value that changed.
Cricket analogy: mutableStateOf's read-tracking is like only the batsman facing the current ball reacting when the umpire signals a wide, while fielders in the deep who weren't involved in that delivery don't move at all.
The by Delegate and Property Syntax
Writing state.value everywhere is verbose, so Compose provides a Kotlin property delegate: var count by remember { mutableStateOf(0) }. The by keyword uses Kotlin's delegated-property feature so that reading count transparently reads state.value, and assigning count = 5 transparently calls state.value = 5. This requires the import androidx.compose.runtime.getValue and import androidx.compose.runtime.setValue extension functions, which is a common source of 'unresolved reference' compile errors for newcomers who forget those imports.
Cricket analogy: The by delegate turning count = 5 into state.value = 5 is like a scorer's shorthand notation that automatically updates the full ledger entry when they jot down a quick tally mark.
import androidx.compose.runtime.*
@Composable
fun LikeButton() {
var likeCount by remember { mutableStateOf(0) }
var isLiked by remember { mutableStateOf(false) }
IconButton(onClick = {
isLiked = !isLiked
likeCount += if (isLiked) 1 else -1
}) {
Icon(
imageVector = if (isLiked) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
contentDescription = "Like ($likeCount)"
)
}
}Recomposition Scope and Skipping
Recomposition happens at the granularity of composable function calls, not the whole tree. If a parent composable reads no state itself but merely passes a state value down as a parameter to a child, only the child that actually reads .value recomposes — the parent's own body is 'skipped' because Compose determined its inputs (the state object reference, not its contents) haven't changed in a way that matters, thanks to structural equality checks. Functions marked with the compiler-inferred @Stable or @Immutable contract are especially good candidates for skipping, since Compose can trust their equals() to reliably detect real changes.
Cricket analogy: A parent passing a score value down to a child without reading it itself is like the team manager relaying the scoreboard number to the announcer without personally recalculating it, so only the announcer's booth updates, not the manager's own notes.
@Composable
fun ProfileScreen(nameState: MutableState<String>) {
// This Column does NOT read nameState.value, so it is not
// recomposed when the name changes -- only NameLabel is.
Column {
Header()
NameLabel(nameState) // reads .value internally
Footer()
}
}
@Composable
fun NameLabel(nameState: MutableState<String>) {
Text(text = nameState.value)
}A useful analogy: mutableStateOf is like a spreadsheet cell with automatic dependency tracking. Any formula (composable) that reads the cell gets recalculated when the cell changes; formulas that never referenced the cell are left untouched, no matter how the sheet updates elsewhere.
A frequent mistake is mutating a List or MutableList held inside a mutableStateOf without reassigning the state — e.g. calling list.value.add(item). Because the reference to the list object doesn't change, Compose's state observer never fires and the UI silently fails to update. Use mutableStateListOf, or reassign with a new immutable list (list.value = list.value + item), instead.
- mutableStateOf creates an observable MutableState<T>; reading .value during composition subscribes that composable to future changes.
- Recomposition is scoped to the specific composable functions that actually read a changed state value, not the entire UI tree.
- The
bydelegate syntax requires importing getValue/setValue extension functions to work with property-style access. - Composables that don't read a piece of state can be skipped during recomposition even if that state is passed down through them.
- Mutating a list in place inside mutableStateOf without reassigning the state reference will not trigger recomposition.
- mutableStateListOf or explicit reassignment with a new list are the correct ways to make list changes observable.
Practice what you learned
1. What determines which composables get recomposed when a MutableState's value changes?
2. What imports are required to use the `by` delegate syntax with mutableStateOf?
3. Why might calling `myListState.value.add(item)` fail to update the UI?
4. What is the correct way to make a mutable list's changes trigger recomposition?
5. If a parent composable receives a MutableState as a parameter but never reads its .value, only passes it further down, what happens to the parent when the value changes?
Was this page helpful?
You May Also Like
State and remember
Understand how Compose tracks mutable values across recompositions using the remember function, and why state that isn't remembered gets lost.
State Hoisting
Learn the Compose pattern of moving state up to a caller so composables become stateless, reusable, and easier to test.
derivedStateOf and Side Effects
Learn how derivedStateOf avoids unnecessary recomposition for computed values, and how effect handlers like LaunchedEffect manage side effects safely.
List Performance and Recomposition
Learn how Compose decides which list items to redraw, and how keys, stable types, and lambdas keep LazyColumn scrolling smoothly under real-world data loads.