DevBits
DevBitsAndroid Handbook
Jetpack Compose

What is the Compose snapshot system?

Understand how Compose tracks state reads and changes while keeping snapshot-backed state consistent.

CMP-013Advanced~45 sec

What the interviewer is testing

The interviewer is usually testing whether you understand why Compose can react to state changes automatically, rather than treating mutableStateOf as a magic variable that somehow causes recomposition.

A strong answer connects two ideas:

  • Compose can see which snapshot-backed state was read.
  • When that state changes, Compose can tell which work depended on it.

The deeper part is understanding why it is called a snapshot in the first place. Compose also needs a consistent view of state while changes are happening. You do not want one part of the UI seeing a change halfway through while another part is still working with the old values.

❌ Common mistake

If they ask you to elaborate

Start with the simplest possible example:

var count by remember {
    mutableStateOf(0)
}

Text("Count: $count")

When Compose runs Text("Count: $count"), it reads count.

That read is important.

Conceptually, Compose can remember:

this piece of UI

read count

Later:

count++

changes that snapshot-backed state.

Now Compose has enough information to connect the two:

count changed

who read count?

this part of the UI

that work may need to run again

That is why mutableStateOf is different from an ordinary Kotlin variable. It participates in a state system that can observe reads and writes.

So far, that explains how Compose notices state. But there is another reason the word snapshot matters.

Imagine a screen has two related pieces of state:

var firstName by mutableStateOf("Ada")
var lastName by mutableStateOf("Lovelace")

Now some work needs to change both values.

Imagine both values are being updated as part of the same piece of work. We do not want other code seeing an awkward halfway state where the first name is new but the last name is still old.

A snapshot gives that work a consistent view of the state. Changes can stay out of sight while they are being made and then become visible together when they are ready.

A useful way to picture that is:

current state

work inside a snapshot

make related changes

apply

new state becomes visible

In normal Compose code, you rarely manage any of this yourself. The state APIs you already use sit on top of the snapshot system and do the work for you.

This also explains other Compose state APIs.

For example:

derivedStateOf {
    listState.firstVisibleItemIndex > 0
}

can know which state it depends on because the state reads inside its calculation are observable.

Likewise:

snapshotFlow {
    listState.firstVisibleItemIndex
}

can observe which snapshot state was read and react when those dependencies change.

They solve different problems, but underneath they both depend on the same useful trick: Compose can see which state was read and react when that state changes.

Production thinking

In production, the snapshot system becomes much easier to reason about when you ask a simpler question:

Where can this state actually change in a way Compose can see?

Imagine a checkout form:

data class CheckoutForm(
    var email: String,
    var phone: String,
)

var form by mutableStateOf(
    CheckoutForm(
        email = "",
        phone = "",
    )
)

Then somewhere in the screen:

form.email = newEmail

This code looks reasonable at first glance. form is inside mutableStateOf, so it is easy to assume Compose will notice any change inside it.

The catch is that Compose is watching form itself. Changing email inside the same CheckoutForm object does not assign a new value to form, so the change does not cross the state boundary Compose is watching.

One option is to make the model immutable:

data class CheckoutForm(
    val email: String,
    val phone: String,
)

var form by mutableStateOf(
    CheckoutForm(
        email = "",
        phone = "",
    )
)

form = form.copy(
    email = newEmail,
)

Now the change is obvious to Compose: form receives a new value.

Another valid design is to make the individual fields deliberately observable:

class CheckoutFormState {
    var email by mutableStateOf("")
    var phone by mutableStateOf("")
}

Here, email and phone are themselves Compose state, so changing either one is something Compose can observe.

Neither pattern is automatically the right one for every screen.

The useful production question is:

"Where do we want observable mutation to live?"

If a screen uses immutable UI models, replacing the model can make changes easy to follow and test. If a dedicated UI-state holder owns fine-grained editing state, individual snapshot-backed properties may be a better fit.

The troublesome version is usually the one in the middle: a deeply mutable object where some changes are Compose state and others are just ordinary Kotlin mutations. That makes it much harder to tell which updates the UI can actually see.

This is also why not every piece of application state needs to become mutableStateOf.

A ViewModel may already expose:

StateFlow<UiState>

and that can remain the application's state model. Compose can observe it at the UI boundary. The snapshot system does not replace Flow, repositories, or the rest of the app architecture. It is simply how Compose keeps track of the state it is working with.

That is usually the practical value of understanding snapshots: state becomes easier to design, and bugs where the UI "didn't notice" a change become much easier to explain. Most Android code will benefit from that understanding without ever calling a low-level Snapshot API.

Follow-up questions

What separates a senior answer

Key takeaways

Learn more

On this page