← Back to list

5 Newbie Compose State Mistakes That Break Your UI

State is how Compose knows when to redraw your UI, and getting it slightly wrong is why your screen freezes, resets, or shows the wrong…

Christophy Barth · 2026-05-25 11:15 · 0 claps · 4.3 min read
#android-development #mobile-app-development #jetpack-compose #kotlin #software-development
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

5 Newbie Compose State Mistakes That Break Your UI

State is how Compose knows when to redraw your UI, and getting it slightly wrong is why your screen freezes, resets, or shows the wrong thing. Here are five state mistakes new Compose developers run into, along with the fixes.

1. Using a Regular Variable Instead of State

When you store a value in a normal variable, Compose has no idea it ever changed:

@Composable
fun Counter() {
    var count = 0

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

The click handler really does run, and count really does go up. The problem is that Compose only redraws when it sees a piece of state change, and a plain var isn't something it watches. Nobody tells Compose anything happened, so the text on screen sits at 0 forever.

Wrap the value in mutableStateOf so Compose can watch it:

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

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

Now count is observable. When it changes, Compose recomposes the parts that read it, and the text updates. You'll notice remember snuck in there too, which is the next mistake.

2. Forgetting to Wrap State in remember

mutableStateOf on its own gets recreated every time the composable runs:

@Composable
fun Counter() {
    var count by mutableStateOf(0)

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

This one is sneaky because it looks correct. You click the button, count becomes 1, and that change triggers a recomposition. But a recomposition runs the function again from the top, which hits mutableStateOf(0) again and builds a brand new state holder starting back at 0. Your counter jumps to 1 and then refuses to go any higher.

remember keeps the same state holder alive across recompositions:

var count by remember { mutableStateOf(0) }

Think of it as two separate jobs. mutableStateOf makes a value observable, and remember makes that value survive the next recomposition. You almost always need both together.

3. Using remember When You Need rememberSaveable

remember survives recomposition, but it does not survive the screen being recreated:

@Composable
fun NameField() {
    var name by remember { mutableStateOf("") }

    TextField(value = name, onValueChange = { name = it })
}

This works fine until the user rotates the device or switches apps and comes back to find Android has rebuilt the activity. When that happens, remember starts over from scratch, and the half-typed text is gone. It feels like a bug in your app, and the user will agree.

rememberSaveable stores the value in the saved instance state, so it comes back:

var name by rememberSaveable { mutableStateOf("") }

It handles primitives and strings out of the box, plus anything Parcelable. For your own custom type, you'll need to give it a Saver (that’s the old way… You can use the retain API), but for ordinary form fields like this, swapping in rememberSaveable is the whole fix.

4. Mutating a List Instead of Replacing It

Calling .add() on a list changes its contents, but the state still points at the same list object:

@Composable
fun TodoList() {
    val items = remember { mutableStateOf(mutableListOf<String>()) }

    Column {
        Button(onClick = { items.value.add("New task") }) {
            Text("Add")
        }
        items.value.forEach { Text(it) }
    }
}

Compose decides whether to recompose by checking if the value of the state changed. Here, the value is a reference to a list, and that reference is exactly the same before and after .add(). The list got longer, but as far as Compose can tell, nothing moved, so the new task never appears.

mutableStateListOf gives you a list that Compose watches directly:

@Composable
fun TodoList() {
    val items = remember { mutableStateListOf<String>() }

    Column {
        Button(onClick = { items.add("New task") }) {
            Text("Add")
        }
        items.forEach { Text(it) }
    }
}

Now, adding, removing, or clearing items all trigger a recomposition on their own. If you’d rather keep a plain list, the other option is to assign a whole new list (items.value = items.value + "New task"), which gives Compose a different reference to notice. Either of them works, but mutableStateListOf feels smoother to think about.

5. Storing Values You Could Just Compute

Here fullName is its own piece of state, kept in sync by hand on every keystroke:

@Composable
fun NameForm() {
    var firstName by remember { mutableStateOf("") }
    var lastName by remember { mutableStateOf("") }
    var fullName by remember { mutableStateOf("") }

    Column {
        TextField(
            value = firstName,
            onValueChange = {
                firstName = it
                fullName = "$firstName $lastName"
            }
        )
        TextField(
            value = lastName,
            onValueChange = { lastName = it }
        )
        Text(fullName)
    }
}

Do you see the bug? The first field updates fullName, but the second field forgot to. Type a last name, and the displayed name will be wrong. This is what extra state costs you: every place that touches firstName or lastName now also has to remember to update fullName, and the day you miss one spot, the screen lies to the user.

If a value can be worked out from another state, why not just compute it?

@Composable
fun NameForm() {
    var firstName by remember { mutableStateOf("") }
    var lastName by remember { mutableStateOf("") }
    val fullName = "$firstName $lastName"

    Column {
        TextField(value = firstName, onValueChange = { firstName = it })
        TextField(value = lastName, onValueChange = { lastName = it })
        Text(fullName)
    }
}

fullName is now a plain val that gets recalculated on every recomposition. Since changing either name already causes a recomposition, the computed value is always current, and there's no sync code to forget. One source of truth beats three. (If the computation were genuinely expensive, derivedStateOf is the next tool to reach for, but for a value like this, a normal val is enough.)

Getting State Right From the Start

Most beginner state bugs come down to two ideas. Compose only redraws when it sees a state value change, and state only sticks around if you tell it to. Almost everything above is one of those two ideas being broken.

When something feels off, the symptom usually points straight at the cause:

  • The UI doesn’t update even though you know the data changed: you’re probably looking at mistake 1, 2, or 4.
  • The screen resets after a rotation: that’s mistake 3.
  • Two parts of the screen disagree with each other: that’s mistake 5.

May your state survive every recomposition and your UI stay in sync :)


메타데이터
post_id
91e2cd0c3dbe
slug
5-newbie-compose-state-mistakes-that-break-your-ui-91e2cd0c3dbe
url
https://medium.com/@christophybarth/5-newbie-compose-state-mistakes-that-break-your-ui-91e2cd0c3dbe
canonical_url
https://medium.com/@christophybarth/5-newbie-compose-state-mistakes-that-break-your-ui-91e2cd0c3dbe
author_url
https://medium.com/@christophybarth
status
ok
fetched_at
2026-06-09 14:34:10