Choosing state persistence in Jetpack Compose: rememberSaveable vs ViewModel with SavedStateHandle vs external storage
A decision guide for persisting Jetpack Compose UI state across configuration changes and process death, comparing rememberSaveable, ViewModel with SavedStateHandle, and external persistence with trade-offs and a concrete hoisting pattern.
13 Aug 2025, 08:31 UTC

The problem: UI state that disappears on rotate or process death
You need transient UI state such as scroll position, form input, selection and expanded flags to survive configuration changes and process death without pushing every UI detail into the business layer. Compose recomposition semantics mean state must live in the right holder, or it is recreated.
The practical takeaway is to hoist business-relevant UI state to a ViewModel backed by SavedStateHandle and keep purely local ephemeral UI in rememberSaveable. Use durable external persistence only for state that must survive app restart.
Decision constraints
- Survive configuration change without re-fetching data.
- Survive process death, which Android can trigger under memory pressure.
- Minimal boilerplate for UI-local state.
- Testable state logic separated from composition.
- Avoid Bundle size limits and unnecessary I/O for transient UI.
Options compared
| Strategy | Scope | Config change | Process death | Type constraints | Typical use |
|---|---|---|---|---|---|
| rememberSaveable | Composable | Yes via SavedStateRegistry | Yes via SavedStateRegistry | Bundle-savable types or custom Saver | Local UI flags, text field, selection |
| ViewModel + SavedStateHandle | Screen / feature | Yes via ViewModelStore | Yes if initialized from SavedStateHandle | Any type supported by SavedStateHandle | Business-relevant UI state, form models |
| External persistence | App | Yes | Yes, survives restart | Any, async | User preferences, drafts that must persist |
Trade-offs
rememberSaveable
Simplest for UI-local primitives. It writes to the SavedStateRegistry and is restored automatically across config change and process death. No ViewModel required.
Limitations: values must be Bundle-savable. Arbitrary objects need a custom Saver. Large payloads risk hitting Bundle size limits. State is not shared across screens and is lost on navigation.
ViewModel with SavedStateHandle
ViewModel survives configuration change via lifecycle retention. Alone it does not survive process death. Adding SavedStateHandle provides process death recovery while keeping state testable outside composition.
Use StateFlow in the ViewModel and collect in Compose with collectAsStateWithLifecycle to respect lifecycle. This centralizes logic and avoids recomposition churn.
Risk: relying on ViewModel without SavedStateHandle causes data loss on system kill.
External persistence
DataStore or Room survives process death and app restart. It is asynchronous and durable, which adds I/O, error handling and complexity.
Overkill for transient UI state such as expanded/collapsed flags. Appropriate for drafts or settings the user expects to keep.
Concrete pattern
Business-relevant state in ViewModel
Initialize from SavedStateHandle and expose immutable state.
// ViewModel
class FormViewModel(savedStateHandle: SavedStateHandle) {
var uiState by mutableStateOf(
FormState(
query = savedStateHandle.get<String>(\"query\") ?: \"\",
selectedId = savedStateHandle.get<Int>(\"selectedId\") ?: -1
)
)
private set
fun onQueryChange(q: String) {
uiState = uiState.copy(query = q)
savedStateHandle[\"query\"] = q
}
}
In Compose:
@Composable
fun FormScreen(viewModel: FormViewModel = viewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
// UI reads state, calls viewModel.onQueryChange
}
SavedStateHandle writes are synchronous to the saved state bundle. The ViewModel remains testable without Compose.
Local ephemeral UI with rememberSaveable
Keep flags that do not belong in business logic local to the composable.
@Composable
fun ItemList() {
var expanded by rememberSaveable { mutableStateOf(false) }
// UI toggles expanded
}
For non-primitive types provide a Saver. Do not store large lists or bitmaps here.
Verification
Rotate the device to trigger a configuration change and confirm UI state is retained without re-fetching data.
Force process death via Developer options → Don’t keep activities or use adb to kill the process, then relaunch the app to verify SavedStateHandle and rememberSaveable restore values correctly.
Observe recomposition behavior with Compose tooling to ensure state collection does not cause unnecessary recompositions. Mixing holders can create duplication and an inconsistent source of truth if hoisting boundaries are unclear.
Limitations to keep in mind: rememberSaveable cannot save arbitrary objects without a custom Saver and may fail with data exceeding Bundle size limits. ViewModel alone does not survive process death. External persistence introduces async handling and should be reserved for durable state.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.