Keys in LazyColumn

Jetpack Compose Mid-level Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Predict the output

The list is A, B, C and row B is expanded. Item A is deleted. Without keys, which row shows as expanded?

@Composable
fun Rows(items: List<String>) {
    LazyColumn {
        items(items) { item ->
            var expanded by remember { mutableStateOf(false) }
            Text(item + if (expanded) " (open)" else "",
                Modifier.clickable { expanded = !expanded })
        }
    }
}
Options
Hint 1 of 2

Without keys, what identifies a row?

Hint 2 of 2

Which item is at position 1 after the delete?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A C: the open state stays at position 1, and C moves into position 1

Correct. Without keys, state belongs to the position. B moves up to position 0 (closed) and C takes position 1 (open).

Why not the others?
  • B B, because remember follows the item. Only with a key does state follow the item.
  • C None, because deleting resets all state. State at positions that still exist is kept.
  • D B and C both. Each position has one state; only position 1 was open.

30-second answer

Without keys, items are identified by position. When you insert or remove an item, the state of each row (a checkbox, an expanded flag, remember values) stays at its old position and ends up on the wrong item, and more rows recompose. items(list, key = { it.id }) ties state and animations to the item itself.

Practise saying it · 30 s. No recording, just a timer

Explain why lazy lists need keys.

Say your answer out loud, then check which of these points you covered:

  • Without keys, state follows positions
  • Keys keep state on the right item
  • Needed for item animations
  • contentType for reuse

Deep dive

Keys must be unique and stable, and must be saveable types (Bundle-friendly) for rememberSaveable inside items.

Keys enable Modifier.animateItem() for insert, remove and move animations.

contentType lets the list reuse compositions between items of the same kind.

Never use the index as the key; it's what you get without one.

Stable keys Compile-checked · Kotlin 2.4.21
data class Row(val id: Long, val title: String)

@Composable
fun KeyedRows(rows: List<Row>) {
    LazyColumn {
        items(rows, key = { it.id }, contentType = { "row" }) { row ->
            Text(row.title, Modifier.animateItem())
        }
    }
}

Common mistakes

  • Using a random UUID per recomposition as the key.
  • Using the index as the key.
  • Keeping per-row state in remember without keys and seeing it jump rows.

Senior take & follow-ups

Tie keys to correctness (state on the right row), then performance and animation.

Follow-up questions

Think of your answer first, then open the hint.

  1. What does contentType help with?

    Show hint

    Reusing compositions between items of the same type, like headers and rows.

  2. What happens with duplicate keys?

    Show hint

    The list crashes.

Similar on iOS: View identity: if/else vs a modifier SwiftUI ForEach needs Identifiable ids for the same reason.

Suggest a fix if something here is wrong or out of date.