Pocket Studio Academy
HomePart 33.5

State and remember

Full course12 min read·4 questions

A counter that refuses to count is the rite of passage of every Compose beginner. Learn exactly why a plain var fails, what mutableStateOf and remember each fix, how the by delegate works, and which one survives turning the phone sideways.

The counter that will not count

Here is a perfectly reasonable-looking screen. It is also completely broken.

BrokenCounter.ktkotlin
1@Composable
2fun BrokenCounter() {
3    // This does not work. Read on.
4    var rolls = 0
5
6    Column {
7        Text(text = "Rolls: $rolls")
8        Button(onClick = { rolls++ }) {
9            Text(text = "Roll")
10        }
11    }
12}

It compiles. It installs. You tap the button and the screen says Rolls: 0. You tap it twenty more times. Rolls: 0.

Nothing you know so far explains that, and guessing at it is where most people's first week goes. So let us take it apart properly, because the answer is the foundation of everything else in Compose.

Think of it like this

Imagine a signwriter who repaints a shop sign from scratch every time the price changes.

They are fast and they are accurate, but they have one quirk: they never look at the old sign. They read the price off a slip of paper and paint a brand new board from nothing.

Now suppose the shopkeeper scribbles the new price on the back of their own hand instead of on the slip. Two things go wrong.

  1. The signwriter is never told to repaint, so the old board stays up.
  2. Even if someone did ask them to repaint, they would read the slip — which still says the old price — because the note on the hand was never part of the slip in the first place. And when they finish, they wash their hands.

mutableStateOf is what makes the slip ring a bell when it changes. remember is what makes the note survive the hand-washing.

You need both, and they fix genuinely different problems.

Problem one: nobody told Compose anything changed

Remember what a really is: an ordinary function. BrokenCounter() runs once, produces a description of a screen, and finishes. It is not sitting there watching a variable.

When you tap the button, rolls++ runs and the number in memory really does become 1. But there is no wire from that variable to the screen. Compose has no idea anything happened, so it never re-runs BrokenCounter, so the Text that was drawn with 0 is still the Text on the glass.

Compose only re-runs a function — a process called — when a value it knows how to watch changes. Ordinary Kotlin variables are not watchable.

The fix is a special kind of holder:

kotlin
val rolls = mutableStateOf(0)

mutableStateOf gives you a MutableState<Int>: a small box with a .value inside it. It is not the number — it is a box holding the number, and Compose watches boxes.

  • When a composable reads rolls.value while it runs, Compose quietly writes down "this function depends on that box".
  • When anything writes to rolls.value, Compose looks up every function that read it and schedules them to run again.

That is the whole subscription mechanism. Reading is subscribing.

Problem two: local variables do not survive the re-run

Suppose we only fix problem one:

kotlin
1@Composable
2fun StillBroken() {
3    // Still wrong, in a new and more interesting way.
4    val rolls = mutableStateOf(0)
5
6    Column {
7        Text(text = "Rolls: ${rolls.value}")
8        Button(onClick = { rolls.value++ }) {
9            Text(text = "Roll")
10        }
11    }
12}

Now the tap does trigger a recomposition. And the screen still says Rolls: 0. Worse — it flickers to 1 on some devices and snaps straight back.

Here is why, and it is the bit that clicks late for most people:

When Compose re-runs your function, it runs the whole function. Line one included.

Line one is val rolls = mutableStateOf(0). So the re-run throws away the box that held 1 and builds a brand new box holding 0. The screen is redrawn from a value that was reset half a millisecond ago.

This is not Compose being strange. It is just how functions work. Every local and inside any function is created fresh on every call — that has always been true, in every language you will ever use. What is new is that your function is called again, by something other than you, possibly sixty times a second.

remember: a pigeonhole that survives the re-run

kotlin
val rolls = remember { mutableStateOf(0) }

takes a and does something very specific:

  • The first time this line runs, it calls your lambda, keeps the result in a slot inside Compose's own memory, and returns it.
  • Every time after that, it ignores the lambda completely and hands back the value already in the slot.

So the box is built exactly once. Every later run of the function gets the same box, still holding whatever it was holding. The hand-washing no longer matters, because the note is in a pigeonhole now.

Two things follow that are worth saying out loud:

  • remember on its own does not cause redraws. remember { 0 } gives you a value that survives but that nothing watches.
  • mutableStateOf on its own does not survive. It gives you a watchable box that gets rebuilt every run.

You almost always want both. That is why the two words appear together so often that they look like a single keyword. They are not.

The by delegate: the version you will actually write

rolls.value everywhere gets noisy fast. Kotlin has a feature for exactly this, and Compose is built to use it:

kotlin
1var rolls by remember { mutableStateOf(0) }
2
3Text(text = "Rolls: $rolls")
4// ...
5rolls++

No .value anywhere. rolls now reads and writes like a plain Int.

by is Kotlin's property delegate keyword — the same one from Lesson 1.18. It means: this name does not store anything itself; hand every read and every write to that object over there. Reading rolls calls the box's getValue; assigning to rolls calls its setValue. Compose's subscription still happens, because underneath it is still .value.

Two rules come with it:

  1. Use var, not val. val rolls by remember { ... } compiles but you can never assign to it — Val cannot be reassigned. With by, the var/val choice describes whether you may assign, and you almost always want to.
  2. Two extra imports. androidx.compose.runtime.getValue and androidx.compose.runtime.setValue. Miss them and you get one of the ugliest error messages in Android; it is first in the Error Doctor below, so you will recognise it.

Three ways of writing the same thing, for reference:

Written asRead it asWrite it as
val n = remember { mutableStateOf(0) }n.valuen.value = 1
var n by remember { mutableStateOf(0) }nn = 1
val (n, setN) = remember { mutableStateOf(0) }nsetN(1)

The middle one is the house style of this course and of the Compose documentation. The third is handy when you want to pass the setter to a child composable — you will meet it again in Part 4.

Putting it together

RollCounter.ktkotlin
1@Composable
2fun RollCounter() {
3    var rolls by remember { mutableStateOf(0) }
4    var die by remember { mutableStateOf(5) }
5
6    Column(
7        modifier = Modifier
8            .fillMaxSize()
9            .padding(24.dp),
10        verticalArrangement =
11            Arrangement.spacedBy(12.dp),
12        horizontalAlignment =
13            Alignment.CenterHorizontally
14    ) {
15        Text(text = "You rolled: $die")
16        Text(text = "Rolls this game: $rolls")
17        Button(
18            onClick = {
19                die = Random.nextInt(1, 7)
20                rolls++
21            }
22        ) {
23            Text(text = "Roll")
24        }
25    }
26}
9:41▲ ▮
You rolled: 3
Rolls this game: 4
Roll

RollCounter() after four taps. Only the two numbers ever change.

Turning the phone sideways loses everything

Try it: run the counter, tap up to 7, then rotate the phone. Rolls this game: 0.

Rotation destroys and rebuilds the — Lesson 2.2's , in the flesh. Compose's memory of your remember slots goes with it, because those slots lived inside the screen that just died.

For small values there is a drop-in fix:

kotlin
var rolls by rememberSaveable { mutableStateOf(0) }

rememberSaveable does everything remember does, and additionally writes the value into the same saved-state bundle Android already uses to restore a rotated screen. Rotate now and the count is still 7.

It has limits, and they are worth knowing before you rely on it:

  • The value must be something Android knows how to store: Int, Long, Float, Boolean, String, arrays and lists of those, or a Parcelable. Your own needs extra work — Part 4 shows the two ways.
  • It is for small things. A saved bundle is not a database; a big list belongs in , not in rememberSaveable.
  • It does not replace a . Once a screen has more than two or three pieces of state and any real logic, the state should move out of the composable entirely. That is Lesson 4.1 and 4.2, and it is the single biggest step up in this course.

For now: remember by default, rememberSaveable when losing the value on rotation would actually annoy someone.

Remembering with a key

One more form, because you will need it in Part 5:

kotlin
1val shuffled = remember(seed) {
2    (1..6).shuffled()
3}

remember(seed) means "keep this, unless seed changes — if it does, throw it away and build it again". Without a key, remember holds on forever. With one, you get a value that rebuilds exactly when its input does.

Careful

Do not reach for remember to fix something that is not state. If a value can be worked out from the parameters you were given — val isWinning = score >= 30 — just calculate it. It is one comparison, it is always correct, and it can never fall out of step. State is for information that cannot be derived: what the user typed, what the dice landed on, whether the sheet is open.

Try it in Pocket Studio
  1. Open ComposeLab and open MainActivity.kt.
  2. Type the BrokenCounter version from the top of this lesson and call it from setContent. Tap Run, then tap the button five times. Nothing moves. Sit with it for a second — this is the bug you are learning to recognise instantly.
  3. Change line one to val rolls = remember { mutableStateOf(0) }, and change the two uses to rolls.value. Run again. It counts.
  4. Now rewrite it in the by form: var rolls by remember { mutableStateOf(0) }, $rolls, and rolls++. Pocket Studio should offer the getValue and setValue imports — accept both. Run. Same behaviour, much less noise.
  5. Delete just the word remember and the braces, leaving var rolls by mutableStateOf(0). Run and tap. Watch it stick at 0 or flicker. Put remember back.
  6. Add the die value and the Random.nextInt(1, 7) from RollCounter, and Run. You now have the beating heart of Dice Duel.
  7. Rotate the phone. Watch the count reset. Change remember to rememberSaveable on the rolls line only, Run, and rotate again — one number survives and one does not, which makes the difference impossible to forget.
Error Doctor5 common errors
e: Type 'MutableState<Int>' has no method 'getValue(Nothing?, KProperty<*>)' and thus it cannot serve as a delegate
MeansYou used by, but the two functions that make by work on Compose state have not been imported. Kotlin is telling you it does not know how to read through this delegate.
FixAdd import androidx.compose.runtime.getValue and import androidx.compose.runtime.setValue. You need both — one for reading, one for writing. This is the single most common Compose import mistake.
e: Val cannot be reassigned
MeansYou wrote val rolls by remember { mutableStateOf(0) } and then tried rolls++. With a delegate, val and var describe whether you may assign to the name.
FixChange val to var. The box itself is still the same box — you are not making anything less safe.
e: Type mismatch: inferred type is MutableState<Int> but Int was expected
MeansYou used = rather than by, so the name refers to the box, not the number inside it. rolls + 1 is asking to add 1 to a box.
FixEither add .value at every use, or switch to by and drop .value everywhere. Pick one style per file and stick to it.
The number on screen never changes, no matter how many times I tap
MeansThe value is being kept, but it is not watchable — usually remember { 0 } with no mutableStateOf, or a plain var at the top of the composable.
FixWrap the initial value: remember { mutableStateOf(0) }. Remember the split: mutableStateOf causes the redraw, remember causes the survival.
The number goes back to its starting value the moment I tap
MeansThe opposite mistake — mutableStateOf(0) with no remember. The tap does trigger a re-run, and the re-run builds a brand new box holding 0.
FixWrap it: remember { mutableStateOf(0) }. If it survives taps but resets when you rotate the phone, that is different and expected — use rememberSaveable.
Recap
  • A is a function, so its local variables are rebuilt on every run. That is why a plain var resets.
  • mutableStateOf(x) makes a watchable box. Reading it subscribes the current composable; writing to it triggers .
  • runs its lambda once and returns the same result on every later run. Without it, the box is rebuilt and the value resets.
  • You need both: var n by remember { mutableStateOf(0) }, plus the getValue and setValue imports.
  • rememberSaveable survives rotation, for small values only.
  • remember(key) rebuilds the value when the key changes.
  • Never store something you can calculate. Two copies of one fact will disagree eventually.
  • Next: what recomposition actually does when it runs — which functions get re-run, which get skipped, and the things you must never put in a composable body.