mutableStateOf: creating observable state and Compose reactivity

Author: IT Sectr Published: 2026-06-28 Reading time: 7 min

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 creates a MutableState container tracked by Compose Runtime
  • Recomposition is triggered automatically when the value of this State object changes
  • Delegation via var allows using mutableStateOf without accessing .value
  • Keys in remember(mutableStateOf) are not needed — State itself notifies Compose of changes
  • Snapshot system guarantees read consistency in multi-threaded environments

What is mutableStateOf in Jetpack Compose

mutableStateOf is a function from the compose.runtime package that creates a MutableState object that stores a value and can notify Compose Runtime about changes. Signature: fun mutableStateOf(value: T, policy: SnapshotMutationPolicy = structuralEquality()): MutableState. The policy parameter determines when a change is considered significant — on structural equality, referential equality, or never.

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.

Syntax and ways to declare mutableStateOf

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.

kotlin
@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.

FormCodeReadWrite
Without delegationval count = mutableStateOf(0)count.valuecount.value = n
With delegationvar count by mutableStateOf(0)countcount = n

Delegated properties and var

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.

kotlin
    // 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 system: how mutableStateOf works internally

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.

Examples of using mutableStateOf

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.

kotlin
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

How is mutableStateOf different from StateFlow?

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.

Can mutableStateOf be used outside @Composable functions?

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.

What happens when State is changed simultaneously from two threads?

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.

How to reset mutableStateOf to its initial value?

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.

Does mutableStateOf affect performance with frequent changes?

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

  • mutableStateOf creates an observable MutableState container tracked by Compose Runtime
  • Delegation via by simplifies code but does not change the mechanics of State
  • Snapshot system guarantees read consistency and prevents unnecessary recompositions
  • Policy determines when a change is considered significant — structuralEquality, referentialEquality, neverEqual
  • remember is mandatory to preserve State between recompositions inside Composable
  • Recommendation: use mutableStateOf with delegation for UI state and referentialEquality for data classes
  • Avoid: creating mutableStateOf without remember — each assignment will create a new object

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.

Discuss the project

Read also