Preserving Compose UI State Across Rotation and Process Death with rememberSaveable
Learn how to preserve Jetpack Compose UI state across configuration changes and process death by splitting ephemeral UI state from business state.
30 Dec 2025, 14:13 UTC

The Problem: State Loss on Rotation and Process Death
When a user rotates an Android device or the system kills your app in the background to reclaim memory, your Jetpack Compose screen can lose critical context. A half-filled form resets to empty, a selected tab jumps back to the default, or an expanded section collapses. Worse, if business state was stored only in composables, the screen may trigger unnecessary network re-fetches or lose a user's in-progress selection entirely.
The fix is not to dump everything into one mechanism. Instead, split state by lifetime and purpose: use rememberSaveable for ephemeral UI-only values, a ViewModel for business state that must survive recomposition, and SavedStateHandle for small business values that must survive process death.
Prerequisites
- A Compose-enabled Android module using
setContentin an Activity or Fragment. androidx.lifecycle:lifecycle-viewmodel-composeandandroidx.lifecycle:lifecycle-viewmodel-savedstatedependencies. Verify exact artifact versions against the AndroidX release notes for your project's Compose compiler and runtime.- UI state exposed from the ViewModel as an immutable data class or individual
Stateobjects. - Events exposed as functions or callbacks, not mutable state hoisted into composables.
Procedure: Assign Each State a Lifetime
1. Hoist Business State to a ViewModel
Move any state that drives data fetching, validation, or domain logic out of composables. The ViewModel survives configuration changes such as rotation, but it does not survive process death. Expose state as immutable values and expose mutations as functions.
class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
private val _selectedCategory = mutableStateOf(
savedStateHandle.get<String>("category") ?: "all"
)
val selectedCategory: State<String> = _selectedCategory
fun selectCategory(category: String) {
_selectedCategory.value = category
savedStateHandle["category"] = category
}
}
In this example, selectedCategory is a small business value. It survives rotation because the ViewModel survives rotation. It survives process death because the ViewModel writes it into SavedStateHandle, which the framework persists into the activity's saved-instance-state Bundle.
2. Use rememberSaveable for UI-Only Draft State
UI-only state—text drafts, expanded/collapsed toggles, scroll positions, selected tabs that do not affect business logic—belongs in rememberSaveable. Compose automatically saves and restores these values through the activity's saved-instance-state Bundle.
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
var queryDraft by rememberSaveable { mutableStateOf("") }
var filtersExpanded by rememberSaveable { mutableStateOf(false) }
val selectedCategory = viewModel.selectedCategory.value
SearchField(
value = queryDraft,
onValueChange = { queryDraft = it },
selectedCategory = selectedCategory,
onCategorySelected = viewModel::selectCategory,
filtersExpanded = filtersExpanded,
onToggleFilters = { filtersExpanded = !filtersExpanded }
)
}
Here, queryDraft and filtersExpanded are transient UI state. They do not need to live in the ViewModel because they do not drive business logic. If the process dies, Compose restores them from the Bundle. If the saved state is missing or invalid, they reset to the provided initial values.
3. Support Custom Types with an Explicit Saver
rememberSaveable natively supports primitives, strings, and Parcelable values. For custom types, provide a Saver that defines how to write the value to a Bundle and restore it.
data class FilterState(val minPrice: Int, val maxPrice: Int)
val FilterStateSaver = Saver<FilterState, Bundle>(
save = { state ->
bundleOf("min" to state.minPrice, "max" to state.maxPrice)
},
restore = { bundle ->
FilterState(
minPrice = bundle.getInt("min"),
maxPrice = bundle.getInt("max")
)
}
)
@Composable
fun FilterSection() {
var filter by rememberSaveable(stateSaver = FilterStateSaver) {
mutableStateOf(FilterState(0, 100))
}
}
If a custom type is not registered with a Saver, or if Saver logic changes incompatibly between app versions, restoration can fail. Always provide a default initial value so the composable can recover gracefully.
What Belongs Where
| State Type | Mechanism | Survives Rotation | Survives Process Death |
|---|---|---|---|
| UI-only drafts, toggles, expanded sections | rememberSaveable |
Yes | Yes (via Bundle) |
| Business state, fetched data, validation | ViewModel |
Yes | No |
| Small business values (IDs, selected filters) | SavedStateHandle |
Yes | Yes |
| Large lists, bitmaps, navigation graphs | Repository / persistent storage | N/A | N/A |
Expected Checks and Verification
After implementing the split, verify behavior under stress:
- Rotation test: Rotate the activity multiple times. Confirm that
rememberSaveablevalues and ViewModel state remain intact, and that no network re-fetch is triggered by rotation alone. - Process-death test: Enable Don't keep activities in Developer Options, background the app, and return to it. Confirm that
rememberSaveabledrafts andSavedStateHandlevalues are restored, while large business state is safely re-loaded from the repository. - Bundle round-trip test: Unit-test custom Savers by saving to a
Bundleand restoring. Assert equality for valid input and default behavior for missing or corrupt keys. - Minification test: Build with R8/ProGuard enabled. Confirm that custom Savers and their state classes are kept and restore correctly. Add keep rules if restoration fails.
Limitations and Recovery Options
rememberSaveable writes into the activity saved-instance-state Bundle. This Bundle has a hard size limit enforced by the Android platform. Storing large objects—lists, bitmaps, or entire domain entities—can cause TransactionTooLargeException or slow restoration. Keep only small, primitive-friendly values in rememberSaveable and SavedStateHandle.
Do not store secrets, authentication tokens, or PII in rememberSaveable or SavedStateHandle without considering that saved instance state may persist on disk beyond the app session. Use encrypted storage for sensitive data.
If a custom Saver fails after an app update because its logic changed, the composable may crash or silently reset. Provide a default initial value in rememberSaveable, and consider clearing incompatible saved state on startup if the schema version changes.
rememberSaveable is not a replacement for a ViewModel. Business state should survive recomposition through the ViewModel, not through UI state saving. Use SavedStateHandle only for the small subset of business values that must survive process death.
Practical Result Check
To confirm the split works, navigate away from the screen, force-stop the app from the device's recent-apps menu, and relaunch. The UI-only draft should reappear exactly as the user left it, the small business value should be restored from SavedStateHandle, and the large business state should reload from the repository without duplication or data loss.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.