Lazy List Keys: Preserve Item State and Animations

Give lazy-list items a stable, unique key whenever they can be inserted, removed, filtered, or reordered. Without one, Compose identifies an item by its position, so remembered child state can stay with the old slot instead of moving with the real data item. Stable keys also enable correct item-move animations and preserve the first visible item when the dataset changes before it.
The official lazy lists guide explains that keys allow Compose to move remembered state together with an item after its position changes. The LazyListScope API reference requires keys to be unique and saveable through Android’s Bundle.
The default identity is position
This list works for a fixed collection, but it has no durable identity if the contents can move:
LazyColumn {
items(tasks) { task ->
TaskRow(task = task, onClick = { onTaskClick(task.id) })
}
}If a task is inserted at the start, every later index shifts. A row with local Compose state—expanded UI, an animation, or a nested scroll position—can then be associated with the wrong task. The fix is not to put business state inside the row; that belongs in the screen state holder. The fix is to give the lazy layout the item’s actual identity.
Add a stable key
Use the model’s durable ID. A database ID, server ID, or a uniquely generated UUID is appropriate when it stays the same for the lifetime of the item.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
data class TaskUi(
val id: String,
val title: String,
val isComplete: Boolean
)
@Composable
fun TaskList(
tasks: List<TaskUi>,
onTaskClick: (String) -> Unit
) {
LazyColumn {
items(
items = tasks,
key = { task -> task.id }
) { task ->
TaskRow(
task = task,
onClick = { onTaskClick(task.id) }
)
}
}
}The key must be unique within that lazy layout. Do not reuse an ID for a header, footer, and data row; prefix keys by kind when those namespaces can overlap.
What a key preserves—and what it does not
| Situation | What a stable key helps Compose do |
|---|---|
| An item moves after sorting | Move remembered item state with the item |
| An item is added before the viewport | Keep the keyed visible item as the first visible item |
A row uses rememberSaveable | Restore saveable state when it returns or the activity recreates |
| A list animates structural changes | Identify which item moved, entered, or left |
A key is not a replacement for application state. Selection, edited data, loading state, and domain decisions should stay in the ViewModel and be rendered as UI state. See state hoisting in Compose for that ownership boundary.
Choose safe key types
The Android documentation calls out an important limitation: the key type must be supported by Bundle to restore rememberSaveable state. Primitive values, strings, enums, and Parcelable values are common choices.
// Good: durable, unique, Bundle-compatible
key = { message -> message.id }
// Risky: changes after sorting or filtering
key = { _, index -> index }
// Wrong: duplicate identity for two different rows
key = { "message" }Do not generate a random value from inside the key lambda. A new value on each composition is the opposite of stable identity.
Keys and item animations
The animateItem() modifier animates additions, removals, and position changes in lazy lists. Compose needs stable keys to know that a particular item moved instead of being replaced by a different item at the same index.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun ReorderableTaskList(tasks: List<TaskUi>) {
LazyColumn {
items(items = tasks, key = { it.id }) { task ->
TaskRow(
task = task,
onClick = {},
modifier = Modifier.animateItem()
)
}
}
}The official guide uses this combination when demonstrating add, remove, and reorder transitions. Test the animation with actual data mutations, not only a static preview.
Common mistakes
Using the list index as a key
An index is valid only when item order and membership are truly fixed. For a feed, search results, drag-and-drop list, or refreshed remote data, it changes precisely when identity matters.
Keying with mutable display text
Titles can be edited, translated, duplicated, or normalized. Use an ID rather than the text displayed to the user.
Confusing list keys with LaunchedEffect keys
They solve different problems. A lazy-layout key identifies a rendered collection item; an effect key controls when an effect restarts. The side-effects guide covers effect keys separately.
Adding keys to a static list by habit
Keys are valuable for dynamic identity. A tiny, never-changing settings list does not need invented IDs. Prefer the simplest code that correctly represents the data.
Test the behavior you care about
Render a list with rows that have a visible local UI state, then insert, remove, and reorder items. Confirm that the state stays with the matching ID. For scroll behavior, place the user partway through a long list and insert an item above the viewport. For animations, exercise real ordering changes. If scrolling is still janky, use the Compose recomposition guide to measure the cause rather than assuming keys solve every performance issue.
FAQ
Do I always need a key in LazyColumn?
No. Add one when the collection can change position or membership and each item has a clear unique identity.
Can I use a data-class object as a key?
Prefer a durable, Bundle-compatible ID. A whole object may be mutable, may not be saveable, and can change equality when a display field changes.
Why did my item animation not look like a move?
Check that each item has a stable, unique key. Without it, Compose may interpret reordered positions as replacement content instead of one item moving.