By default, LazyColumn identifies items by their position in the list. Delete the first message and every remaining row shifts index. Compose diffs that as "every row changed," so remembered state like an expanded row or a text field's contents attaches to the wrong item.
#The old way
@Composable
fun MessageList(messages: List<Message>) {
LazyColumn {
items(messages) { message ->
MessageRow(message)
}
}
}
#The new way
data class Message(val id: Long, val author: String, val body: String)
@Composable
fun MessageList(messages: List<Message>) {
LazyColumn {
items(
items = messages,
key = { it.id }
) { message ->
MessageRow(message)
}
}
}
#Why it matters
- Only the rows that actually changed recompose. Compose tracks items by key across insertions, removals, and reorders.
rememberSaveableand per-itemrememberstate survive reordering, because they're attached to the id rather than the slot position.- Item-move animations (
Modifier.animateItem) read the key to know which item moved where.
#Gotcha
The key must be unique within the list and stable across recompositions. Duplicate keys throw IllegalArgumentException: Key ... was already used.
Deriving the key from content that can change defeats the purpose. Take message.body.hashCode(). Edit two messages into the same text and they collide, and you're back to the same crash.