Persisting Non-Parcelable UI State in Jetpack Compose Across Configuration Changes and Process Death
Keep non-Parcelable UI state alive across configuration changes and process death in Jetpack Compose by defining a custom Saver for rememberSaveable.
19 Jul 2026, 11:14 UTC

The problem: transient UI state disappears on rotation
If you use remember to hold a filter query or a selected tab, that state vanishes when the user rotates the device or when Android kills your activity to reclaim memory. Users see the form reset to defaults, which feels like data loss even though the app is still running. The useful takeaway: for UI state that must survive configuration changes and process death, use rememberSaveable. When your state holder is not directly Bundle-compatible—such as a custom data class—supply a custom Saver that maps it to primitives.
How rememberSaveable works
rememberSaveable saves and restores values through the saved instance state Bundle. A Bundle is a key-value map that Android persists across activity recreation. Under the hood, Compose reads the LocalSavedStateRegistry composition local, registers your value, and writes it into the Bundle when the activity is stopped. When the activity is recreated—whether from a rotation or a process-death restore—the registry reads the Bundle, hands the saved data back to your composable, and re-initializes the remembered state with the restored value.
By default, rememberSaveable supports types that the platform can write into a Bundle: primitives, String, Parcelable, and Serializable. A plain Kotlin data class is none of those, so it needs a Saver.
A worked example: FilterState with a custom Saver
Consider a search filter with a text query and a boolean toggle. The state is small, transient, and belongs to the UI, not to a repository.
data class FilterState(
val query: String,
val active: Boolean
)
val FilterStateSaver = Saver<FilterState, List<Any>>(
save = { state ->
listOf(state.query, state.active)
},
restore = { saved ->
FilterState(
query = saved[0] as String,
active = saved[1] as Boolean
)
}
)
@Composable
fun FilterScreen() {
var filter by rememberSaveable(stateSaver = FilterStateSaver) {
mutableStateOf(FilterState("", false))
}
OutlinedTextField(
value = filter.query,
onValueChange = { filter = filter.copy(query = it) },
label = { Text("Search") }
)
Switch(
checked = filter.active,
onCheckedChange = { filter = filter.copy(active = it) }
)
}
The Saver has two parts. The save lambda converts FilterState into a List<Any> containing only Bundle-safe primitives. The restore lambda converts that list back into a FilterState. Because the saved representation is a list of primitives, Android can write it into the Bundle without reflection or custom marshalling.
The initializer lambda passed to rememberSaveable runs only when there is no saved value to restore. On a configuration change, the composition is torn down and rebuilt, the Saver restores the list from the Bundle, and the mutableStateOf is seeded with the recovered FilterState. No network call or side effect is triggered by the restoration path itself.
Verifying the behavior
You can confirm the state survives both configuration changes and process death with two manual checks.
- Configuration change: Rotate the device or toggle system dark mode. The text field and switch should retain their values without any visible reload.
- Process death: Enable Developer options > Don't keep activities, navigate away from the app, and return. The screen should rebuild with the same
FilterStaterather than defaults.
To prove the initializer is not re-running on restore, add a log line inside the rememberSaveable initializer lambda. If restoration succeeds, that log should appear only on the very first composition, not after every rotation or process-death restore.
Limits and common mistakes
Bundle size is finite
The saved instance state Bundle travels across process boundaries. It has a practical size limit—historically around 1 MB, though the exact threshold is system-dependent. Storing large objects, bitmaps, or deep object graphs in a Saver will eventually throw a TransactionTooLargeException. Keep saved state small: identifiers, flags, and short strings are appropriate; entire lists of downloaded items are not.
Changing the Saver breaks old state
The Saver defines an on-disk contract. If you ship an update that changes the shape of the saved list—for example, adding a third field—previously saved state may fail to restore or crash with a ClassCastException. When you must evolve the schema, write a defensive restore lambda that checks the list size and types before casting, or provide a migration path.
rememberSaveable is not a ViewModel
Saved instance state is for transient UI state only. It is not a replacement for a ViewModel, a local database, or DataStore. If the data must outlive the UI session—such as a user's saved searches or authentication tokens—move it into a ViewModel backed by a repository. A common pattern is to keep the rememberSaveable at the screen level for UI-only state, hoist read-only values down to child composables, and let the ViewModel own business data.
Assuming all data classes are Parcelable
Kotlin data class does not automatically implement Parcelable or Serializable. If you try to pass a plain data class to rememberSaveable without a Saver, the build will fail at runtime when the system attempts to write the Bundle. The custom Saver shown above is the idiomatic fix.
Practical checklist
- Use
rememberfor state that can be recreated cheaply on recomposition. - Use
rememberSaveablefor transient UI state that must survive configuration changes and process death. - Supply a custom
Saverwhenever the state type is not natively Bundle-compatible. - Keep the saved representation minimal and stable across app versions.
- Verify with rotation and Don't keep activities before shipping.
By confining rememberSaveable to small, UI-scoped values and giving non-Parcelable types an explicit Saver, you get predictable restoration without leaking business logic into your composables.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.