State & Recomposition in Compose

Reviewed & published by Brayan K

In Compose, UI is a function of state. Change the state and Compose recomposes — re-running the affected composables to redraw the screen. The trick is making that state observable.

Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end you'll use remember and mutableStateOf, hoist state for reusable composables, and persist state across rotation with rememberSaveable.

What You'll Learn in This Lesson

1️⃣ Observable State with remember + mutableStateOf

A plain var won't update the UI — Compose only watches State objects. mutableStateOf makes the value observable; remember keeps it alive across recompositions. The by delegate lets you read and write it like a normal variable.

import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@Composable
fun Counter() {
    // remember keeps the value across recompositions.
    // mutableStateOf makes it observable so changes trigger recomposition.
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Clicked $count times")
    }
}

Each tap writes to count; Compose notices the read inside the button label and recomposes just that text.

2️⃣ State Hoisting

A reusable composable should be stateless: it receives the current value and an event callback. The stateful parent owns the data. This is the state down, events up pattern.

import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

// STATELESS: receives the value and an event callback (state down, events up).
@Composable
fun CounterButton(count: Int, onIncrement: () -> Unit) {
    Button(onClick = onIncrement) {
        Text("Clicked $count times")
    }
}

// STATEFUL: owns the state and passes it down.
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }
    CounterButton(count = count, onIncrement = { count++ })
}

CounterButton is now decoupled from storage — you can preview it, test it, and reuse it anywhere.

3️⃣ Surviving Rotation with rememberSaveable

Plain remember survives recomposition but not a configuration change like rotation. rememberSaveable persists the value through those too.

import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue

@Composable
fun SurvivingCounter() {
    // rememberSaveable survives recomposition AND rotation
    // (configuration changes), unlike plain remember.
    var count by rememberSaveable { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

Your turn. Replace the TODO, then run and compare.

// 🎯 YOUR TURN — replace the TODO then run in Android Studio.

import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@Composable
fun Toggle() {
    // 1) Make 'on' observable state starting at false.
    //    👉 replace TODO with: remember { mutableStateOf(false) }
    var on by TODO

    Button(onClick = { on = !on }) {
        Text(if (on) "ON" else "OFF")
    }
}

Mini-Challenge: A Live Greeter

Bind a TextField to a String state and watch a greeting recompose as you type.

// 🎯 MINI-CHALLENGE: a name greeter.
//
// 1. Write a @Composable Greeter().
// 2. Hold a String state 'name' with remember { mutableStateOf("") }.
// 3. Show a TextField bound to name (value = name,
//    onValueChange = { name = it }).
// 4. Below it, show Text("Hello, $name!").
//
// ✅ As you type "Ada", the Text recomposes to:
//    Hello, Ada!

@Composable
fun Greeter() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — State

ConceptCompose API
Observable stateremember { mutableStateOf(0) }
Read/write directlyvar n by remember { ... }
Survive rotationrememberSaveable { mutableStateOf(0) }
Hoisting (down)fun C(value: Int, ...)
Hoisting (up)onValueChange: (Int) -> Unit

🎉 Lesson Complete!

Practice quiz

What does it mean for Compose to 'recompose'?

  • It rebuilds the whole app from scratch
  • It re-runs composables affected by changed state to update the UI
  • It recompiles the Kotlin source
  • It restarts the Activity

Answer: It re-runs composables affected by changed state to update the UI. Recomposition re-runs the composables that read changed state, updating just what is needed.

Which function holds a value across recompositions?

  • keep
  • cache
  • remember
  • store

Answer: remember. remember retains a value in the composition so it survives recomposition.

Which API creates an observable state holder Compose can track?

  • mutableStateOf(...)
  • stateOf(...)
  • observable(...)
  • live(...)

Answer: mutableStateOf(...). mutableStateOf creates a MutableState that triggers recomposition when its value changes.

With 'var count by remember { mutableStateOf(0) }', how do you increment count?

  • count.value++
  • count.set(count + 1)
  • count++
  • count.update()

Answer: count++. The 'by' delegate lets you read/write count directly, so count++ works.

What problem does state hoisting solve?

  • It speeds up compilation
  • It moves state up so a composable becomes stateless and reusable
  • It saves state to disk
  • It hides state from the parent

Answer: It moves state up so a composable becomes stateless and reusable. Hoisting lifts state to a caller, making the child stateless, testable, and reusable.

A hoisted, stateless composable typically receives what two things?

  • A Context and a Bundle
  • A ViewModel and a Repository
  • Two lambdas only
  • A value and an onValueChange callback

Answer: A value and an onValueChange callback. The state-down/events-up pattern passes the current value down and an onChange event up.

Which function preserves state across configuration changes like rotation?

  • rememberSaveable { ... }
  • remember { ... }
  • retain { ... }
  • keepState { ... }

Answer: rememberSaveable { ... }. rememberSaveable survives recomposition AND configuration changes (and process recreation) via a saver.

If you use plain 'remember' and rotate the device, what happens to the value?

  • It is encrypted
  • It is reset to its initial value
  • It is doubled
  • It is saved to the database

Answer: It is reset to its initial value. Plain remember does not survive a configuration change, so the value reinitializes.

Why does 'var x = 0' (a normal variable) NOT update the UI when changed?

  • Variables are immutable
  • It is the wrong type
  • Compose only observes State objects, not plain variables
  • It needs the @State annotation

Answer: Compose only observes State objects, not plain variables. Compose recomposes only when it observes a change to a State; a plain var is invisible to it.

Where should you read a state value to make a composable recompose on change?

  • Inside the composable that should update
  • Only in onCreate
  • In a background thread
  • In the manifest

Answer: Inside the composable that should update. Reading a State inside a composable subscribes it, so that composable recomposes when the value changes.

Continue this course

Frequently asked questions

Why do I need both remember and mutableStateOf?

They solve two different problems. mutableStateOf creates an observable State so Compose knows to recompose when the value changes. remember keeps that State object alive across recompositions — without it, every recomposition would create a fresh State and reset to the initial value. You almost always pair them: remember { mutableStateOf(initial) }.

What exactly triggers a recomposition?

Compose tracks which composables read which State values. When you write to a State (for example count++ on a mutableStateOf), Compose schedules recomposition of exactly the composables that read that state. It's smart and fine-grained: unrelated parts of the screen are skipped, which keeps recomposition cheap.

What is state hoisting and why bother?

State hoisting means moving a composable's state up to its caller, so the composable becomes stateless and simply receives a value plus an onValueChange (or onClick) callback. This 'state down, events up' pattern makes composables reusable, easier to test, and gives a single source of truth. The stateful parent owns the data; the stateless child just renders it.

When should I use rememberSaveable instead of remember?

Use rememberSaveable when the value should survive configuration changes such as screen rotation, dark-mode toggles, and even process death — for example the text a user has typed or a selected tab. It stores the value in a Bundle via a Saver. Plain remember only survives recomposition, so the value resets on rotation. For ephemeral, easily recomputed values, plain remember is fine.