MutableState is an interface in Jetpack Compose that represents a container for a mutable observable value. It is the foundation of Compose’s reactive system: every time a MutableState value changes via the setter, Compose Runtime notifies all reading components and triggers recomposition. According to Google Android Developers, 2026, understanding MutableState is essential for correct state management in a declarative UI.
Key Takeaways
MutableState is an interface from the androidx.compose.runtime package that declares a single property: override var value: T. The getter returns the current value, the setter writes a new one and notifies Compose Runtime of the change. The interface inherits from State<T>, where value is read-only. This two-tier architecture allows access separation: a component that only needs to read the value receives State<T>, while the owner component receives MutableState<T>.
The default implementation of MutableState is the internal class SnapshotMutableStateImpl, which uses a snapshot mechanism to track changes. When the value setter is called, the current snapshot records the write and marks all registered ObservedScopes as invalid. These scopes (usually Composable functions) will be recomposed on the next frame. The entire process happens synchronously and without locks thanks to the Lock-free snapshot architecture.
State vs MutableState: State is a read-only interface used for public component APIs. When you declare a Composable function parameter as State<Int>, you guarantee that the component can read but not change the state. MutableState is used inside the owner component. This separation is one of the basic Compose practices that prevents unauthorized changes.
The hierarchy of state interfaces in Compose has several levels. At the top is State<T> with a read-only value. Below is MutableState<T> with a read-write value. Further down are specialized primitive versions: MutableIntState, MutableFloatState, MutableLongState, MutableBooleanState and others, which avoid primitive boxing.
MutableDoubleState and MutableLongState are less common but also existing types. Collection interfaces: MutableListState — for tracking changes inside a list, MutableStateMap — for maps. Each of these interfaces is optimized for a specific scenario and extends the base MutableState with additional collection manipulation methods.
SnapshotStateList and SnapshotStateMap are implementations of mutable lists and maps compatible with snapshots. They allow tracking not just value replacement, but internal changes: adding an item to a list, removing, modifying an existing item. For such structures, mutableStateListOf() and mutableStateMapOf() create the corresponding observable collections.
| Interface | Purpose | Creation method |
|---|---|---|
| State<T> | Read-only container | — |
| MutableState<T> | Read-write container | mutableStateOf() |
| MutableIntState | Primitive Int without boxing | mutableIntStateOf() |
| MutableFloatState | Primitive Float without boxing | mutableFloatStateOf() |
| SnapshotStateList | Observable list | mutableStateListOf() |
| SnapshotStateMap | Observable map | mutableStateMapOf() |
SnapshotMutationPolicy is an interface that determines when a MutableState change is considered significant. mutableStateOf accepts policy as a second argument. Standard implementations: structuralEquality() (equals), referentialEquality() (===), neverEqualPolicy() (always considers a change). Custom logic can be implemented by creating your own policy.
structuralEquality() — default behavior. Compose compares the new value with the old one using equals(). If the result is true, recomposition is NOT triggered. This is convenient for primitives and data classes, where two instances with the same fields are considered equal. The downside: if a data class contains a List, equals() performs a deep comparison, which can be costly for large lists.
referentialEquality() — compares references using ===. Recomposition is triggered only when a different object is assigned, even if the content is identical. This is optimal for immutable data classes where each new instance guarantees a change. neverEqualPolicy() — always considers a change significant without performing comparison. Useful when the setter is called rarely and there is no need to spend time on equals.
// Policy comparison in practice
data class User(val name: String, val age: Int)
@Composable
fun UserProfile() {
// structuralEquality: recomposition ONLY if data changed
var user1 by remember {
mutableStateOf(User("Alice", 30))
}
// referentialEquality: recomposition on ANY assignment
var user2 by remember {
mutableStateOf(User("Bob", 25),
SnapshotMutationPolicy.referentialEquality())
}
// user1: copy() with same fields does NOT trigger recomposition
// user2: even user2.copy() == user2 triggers recomposition (new ref)
}
Primitive MutableIntState and similar are specialized interfaces that store primitives without boxing. A regular MutableState<Int> stores Int as Integer, which creates an object on the heap with every write. MutableIntState stores int (primitive), completely eliminating boxing overhead. This is especially important for high-frequency updates — counters, scroll positions, animation values.
mutableIntStateOf(), mutableFloatStateOf(), mutableLongStateOf() — functions that create primitive MutableState. The interfaces are called MutableIntState, MutableFloatState, MutableLongState. They extend MutableState<Int>, MutableState<Float> and MutableState<Long> respectively, adding the intValue property for fast primitive access. Their internal implementation uses AtomicInteger for lock-free read/write.
Usage: counters (Int), scroll positions (Float offset), timestamps (Long). In most everyday scenarios the performance difference is unnoticeable, but in LazyList with thousands of items and transition animations, primitive State provides a noticeable boost. Google recommends using primitive State for typical scenarios instead of the universal mutableStateOf.
@Composable
fun ScrollCounter() {
// Bad: boxing on every update
var badCount by remember { mutableStateOf(0) }
// Good: no boxing, primitive storage
var goodCount by remember { mutableIntStateOf(0) }
// Usage is identical
Button(onClick = { goodCount++ }) {
Text("Count: $goodCount")
}
}
Consider a TodoList component, where MutableState is used in two forms: as separate variables for input state and as a SnapshotStateList for a dynamic task list. Both use delegation for code brevity.
data class TodoItem(val id: Int, val text: String, val isDone: Boolean = false)
@Composable
fun TodoScreen() {
var inputText by remember { mutableStateOf("") }
val items = remember { mutableStateListOf() }
Column(modifier = Modifier.padding(16.dp)) {
Row {
TextField(
value = inputText,
onValueChange = { inputText = it }
)
Button(onClick = {
if (inputText.isNotBlank()) {
items.add(TodoItem(items.size, inputText))
inputText = ""
}
}) { Text("Add") }
}
LazyColumn {
items(items) { item ->
Row(modifier = Modifier.fillMaxWidth().clickable {
val idx = items.indexOf(item)
items[idx] = item.copy(isDone = !item.isDone)
}) {
Checkbox(checked = item.isDone, onCheckedChange = null)
Text(item.text)
}
}
}
}
}
mutableStateListOf creates a SnapshotStateList — a mutable list that tracks changes to individual elements. When items.add() and items[n] = newValue are called, Compose sees the mutation and recomposes only those LazyColumn elements that changed. inputText is a regular MutableState<String>. The combination of two MutableState types (single and collection) is a typical pattern for screens with forms and lists.
Frequently Asked Questions
MutableState without remember will be created anew on every recomposition. Each new call to mutableStateOf creates a new object, and the old value is lost. Always use remember to persist State between recompositions, unless the State is created outside a Composable (e.g., in a ViewModel).
Read .value once outside a snapshot via snapshot { }. But this disables reactivity — changes will no longer trigger recomposition. For one-time reading without subscription, use currentValue() inside a snapshot without reading.
mutableIntStateOf is faster because it does not require boxing int into Integer. With thousands of updates per second (animation, scrolling), the difference can reach 30-50% in allocation time. For rare updates (clicks, text input), the difference is negligible.
It is possible but not recommended. Instead of MutableState, pass State (read-only) + an onValueChange lambda. This implements the State Hoisting pattern and makes the component reusable. Components that accept MutableState violate the unidirectional data flow.
Implement the MutableState interface and provide override var value with a getter and setter. In the setter, you can add validation or logging. For backward compatibility with Compose Runtime, wrap your custom implementation in snapshotFlow or use snapshotIncrement.
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