remember vs rememberSaveable vs ViewModel: what survives what
Learn which state holder survives rotation, process death, or app closure in Jetpack Compose. Stop losing draft text and scroll positions with the right choice.

Most developers assume that if they declare a variable inside a composable, it will persist as long as the screen is visible. That is the trap. You write var count by remember { mutableStateOf(0) }, expect the counter to hold its value when you rotate the phone or switch dark mode, and then watch it snap back to zero. The issue is not that Compose is broken; it is that you picked a holder that only survives recomposition, while your device performed a configuration change that recreated the entire Activity.
The book Compose Without the Confusion opens its state chapter with exactly this distinction under the heading "How long does state live?". This post covers the four main holders, what each one survives, and the exact commands or settings you need to use to match your data to the right container.
One caveat, straight from the book: this guidance applies to standard Android configuration changes and process death. Your specific app architecture may require additional layers, so check the sections on ViewModels and databases if your state needs to survive beyond the screen.
The trap: confusing recomposition with configuration changes
The usual way people find out their state is too fragile is that they rotate the phone and see their UI reset. The book explains that remember survives only recomposition. Recomposition happens when Compose needs to redraw a composable because its inputs changed, but the Activity itself remains alive. When you rotate the device, switch dark mode, or change the system language, Android recreates the Activity. This destroys the composition tree entirely.
If your state is held in remember, it vanishes with the old Activity. You might think adding mutableStateOf fixes it, but writing val count = mutableStateOf(0) without remember means the holder is recreated on every recomposition anyway, resetting the value immediately. You need both an observable holder and a mechanism to keep it across recreations.
The fix for temporary UI state is rememberSaveable. It is remember that also writes the value into the saved instance state. This allows it to survive configuration changes and process death for Bundle-able values like numbers, strings, booleans, and parcelable types.
var draftName by rememberSaveable { mutableStateOf("") }
var showDeleteDialog by rememberSaveable { mutableStateOf(false) }
A good rule of thumb from the book is to use remember or rememberSaveable for UI element state, such as whether a menu is expanded, the scroll position, or a draft in a text field. These values live in the composable and do not need to persist after the app closes.
The trap: expecting ViewModel to save everything
Many developers move all state into a ViewModel, assuming it will solve all persistence problems. This is partially correct but incomplete. A ViewModel survives configuration changes on its own, meaning your data stays intact when you rotate the phone. However, the book notes that a ViewModel does not survive process death on its own. If the system kills your app while it is in the background, the ViewModel is destroyed along with the Activity.
If your state comes from data or business rules, such as a list of items fetched from an API or whether a save operation succeeded, it belongs in a ViewModel. This keeps your UI composables stateless and testable. You hoist the state up to the lowest common ancestor of everything that reads or writes it.
// Stateless: easy to preview, test and reuse
@Composable
fun AddItemBar(
name: String,
onNameChange: (String) -> Unit,
onAdd: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("New item") },
singleLine = true,
modifier = Modifier.weight(1f),
)
Spacer(Modifier.width(8.dp))
Button(onClick = onAdd, enabled = name.isNotBlank()) { Text("Add") }
}
}
This pattern separates the UI from the state ownership. The parent owns the state and passes it down; the child sends events up. If the state needs to survive the app being closed, you must move it out of the ViewModel and into a persistent storage solution like Room or DataStore.
The trap: mutating objects instead of replacing them
Another common failure mode is assuming Compose will detect changes when you modify an object in place. Compose notices when a State holder gets a new value, but it does not notice when you reach into the old value and change it. Mutable objects that are not observable, such as an ArrayList or a mutable data class, do not trigger recomposition and show stale data.
// Wrong: mutates the list in place, Compose sees the same object
val items = remember { mutableStateOf(mutableListOf<PantryItem>()) }
items.value.add(newItem) // nothing recomposes
The fix is to treat UI state as values. You should replace the value with a new immutable list or use an observable list type provided by Compose. For data classes, you use copy() instead of setting fields directly.
// Right: replace the value with a new immutable list
var items by remember { mutableStateOf(listOf<PantryItem>()) }
items = items + newItem
// Also right: an observable list
val items = remember { mutableStateListOf<PantryItem>() }
items.add(newItem)
// Updating one item in a data class: copy, do not set fields
items = items.map { if (it.id == id) it.copy(quantity = it.quantity + 1) else it }
This ensures that every time the list content changes, Compose receives a new reference and triggers recomposition for the affected components.
Checklist
- Use
rememberfor state that only needs to survive recomposition within the same Activity instance. - Use
rememberSaveablefor UI element state like drafts or scroll positions that must survive rotation and process death. - Remember that
rememberSaveableworks out of the box for Bundle-able types; use a custom Saver for complex objects. - Hoist state to the lowest common ancestor that needs it, passing values down and events up via lambdas.
- Use
ViewModelfor screen state derived from data or business rules that must survive configuration changes. - Know that
ViewModeldoes not survive process death; use Room or DataStore for persistent storage. - Replace mutable lists with immutable lists or
mutableStateListOfto trigger recomposition on change. - Never call suspend functions directly in the composable body; wrap them in
LaunchedEffectwith appropriate keys.


