mutableStateOf is a function in Jetpack Compose that creates a mutable observable state container. When the value inside this container changes, Compose automatically triggers recomposition of all components reading this state. Without mutableStateOf, the UI could not reactively update when data changes. According to Google Android Developers, 2026, mutableStateOf is the primary building block for local state in Compose.
Key Takeaways
mutableStateOf is a function from the compose.runtime package that creates a MutableState
MutableState is an interface with a single property value: a getter for reading and a setter for writing. When the setter is called, Compose Runtime records the change in a snapshot and marks all Composable functions that read this State variable as needing recomposition. This process happens synchronously within a single snapshot cycle, which eliminates intermediate states during cascading changes.
Parameter policy — the second argument of mutableStateOf, defines the comparison behavior. structuralEquality() checks equals() — this is the default behavior. referentialEquality() checks === (referential equality). neverEqual() considers every assignment a change. Choosing a policy affects whether recomposition is triggered when assigning the same value.
The simplest way to declare observable state is to use mutableStateOf with remember. Without remember, each recomposition would create a new State, and all previous changes would be lost. remember ensures that the same MutableState survives a series of recompositions as long as the Composable function remains in the composition.
@Composable
fun Counter() {
// Without delegation: read/write via .value
val count = remember { mutableStateOf(0) }
Button(onClick = { count.value++ }) {
Text("Count: ${count.value}")
}
}
@Composable
fun CounterDelegated() {
// With delegation: var + by = Property Delegation
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
The difference between the two approaches is syntactic. Property Delegation (by) uses a Kotlin convention: the compiler generates getValue() and setValue() calls for reading and writing. This is equivalent to directly accessing count.value but looks like working with a regular variable. Both approaches are functionally identical: Compose tracks reads in the getter and writes in the setter regardless of the notation.
| Form | Code | Read | Write |
|---|---|---|---|
| Without delegation | val count = mutableStateOf(0) | count.value | count.value = n |
| With delegation | var count by mutableStateOf(0) | count | count = n |
The Kotlin delegated property mechanism is not a Compose feature but a built-in language capability. Any class can implement getValue(thisRef, property) and setValue(thisRef, property, value) operators, after which its instance can be used with the by keyword. MutableState works exactly like this: getValue returns the current value, and setValue assigns a new one.
An important distinction: val vs var. mutableStateOf can be assigned to both val and var. With val (val count = mutableStateOf(0)), the MutableState object itself is immutable, but its value property can be changed. With var (var count by mutableStateOf(0)), delegation creates the illusion of working with a primitive, but the setter actually calls setValue on MutableState. Choosing between val and var is choosing between explicit and implicit access to .value.
State Delegation is syntactic sugar that simplifies code but does not change the mechanics. The Kotlin compiler translates var x by mutableStateOf(0) into getter/setter that call mutableStateOf.getValue() and mutableStateOf.setValue(). In the generated bytecode, there is no difference between val and var with by — both work through the same MutableState container.
// Custom delegate for Compose State
class ValidatedState<T>(initialValue: T) {
private val state = mutableStateOf(initialValue)
operator fun getValue(thisRef: Any?, property: KProperty<*>) = state.value
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (value != state.value) {
state.value = value
}
}
}
@Composable
fun Test() {
var text by remember { ValidatedState("") }
}
Snapshot is a Compose Runtime mechanism that ensures State read consistency during parallel changes. When a Composable function reads mutableStateOf, the snapshot records the current value. If during composition another change writes to the same State, the snapshot sees the write but does not allow reading inconsistent data — reading always returns the value valid at the start of the snapshot.
When the setter mutableStateOf.value = newValue is called, Compose Runtime does not immediately trigger recomposition. Instead, the change is registered in the current snapshot. When the snapshot is applied (at the frame boundary), Compose iterates through the list of changed States and marks the reading components as Invalid. Only in the next frame does recomposition start. This guarantees that the UI is not redrawn dozens of times during cascading changes.
Global and local snapshots: by default, mutableStateOf works in the global snapshot, which applies automatically. You can create a local snapshot via Snapshot.takeSnapshot() for isolated reading without side effects. This is used inside Modifier when you need to read State without subscribing to changes. This approach optimizes performance and prevents unexpected recompositions.
Let's consider a real scenario — a login form with three fields: email, password, and loading status. All three fields use mutableStateOf, but with different policies and different nesting levels. email uses delegation, password uses direct access.
data class LoginState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
@Composable
fun LoginForm(onLogin: (String, String) -> Unit) {
// Single State for form, policy = referentialEquality
var formState by remember {
mutableStateOf(LoginState(), SnapshotMutationPolicy.referentialEquality())
}
val isValid = remember(formState) {
formState.email.contains("@") && formState.password.length() >= 6
}
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = formState.email,
onValueChange = { formState = formState.copy(email = it) },
label = { Text("Email") }
)
OutlinedTextField(
value = formState.password,
onValueChange = { formState = formState.copy(password = it) },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation()
)
Button(
onClick = { onLogin(formState.email, formState.password) },
enabled = isValid
) {
Text("Login")
}
}
}
In this example, mutableStateOf is used with a custom data class LoginState and policy referentialEquality. This means recomposition will only trigger when a new LoginState instance is assigned via copy(). isValid is computed based on formState and is recalculated only when it changes. This approach provides clear control over recompositions: each form field changes only through creating a new copy.
Frequently Asked Questions
mutableStateOf is a Compose-specific container that works within snapshots. StateFlow comes from kotlinx.coroutines.flow and is not tied to Compose. mutableStateOf automatically triggers recomposition, while StateFlow requires collectAsState(). For UI state inside Composable, mutableStateOf is preferred.
Yes, mutableStateOf can be called outside Composable functions, but it will not be tracked. For reactivity in the UI, State must be read inside Composable. Many ViewModels use MutableStateField (a wrapper around mutableStateOf) to pass state to the UI via StateFlow.
The Snapshot system guarantees consistency: each recomposition sees a consistent state at the snapshot start. Changes from different threads are applied atomically at the frame boundary, eliminating race conditions during reads within a single composition.
Assign a new value: count.value = 0 (or count = 0 with delegation). If you need to fully recreate the State, use remember with a key: remember(key) { mutableStateOf(initial) } — when the key changes, the State will be created anew.
Composer uses snapshots that group changes: even with hundreds of assignments in a single frame, recomposition runs only once. For very frequent updates (animations), use Animatable or animate*AsState — they are optimized for frame-by-frame updates.
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