Diagnosing Jetpack Compose State Restoration Failures After Process Death
When Compose UI resets to defaults after your app is killed and relaunched, the fault usually lies in remember, missing Savers, or unwired SavedStateHandle. This guide walks through ordered diagnostics, concrete fixes, and escalation criteria.
25 Nov 2025, 02:46 UTC

The problem: state vanishes when the system reclaims your process
After a user backgrounds your app and the system later kills it to reclaim memory, the next launch should restore the previous screen exactly as it was. When a TextField clears its text, a CheckBox unchecks itself, or a custom control snaps back to its default, the state was never wired into the saved‑instance‑state mechanism. This is not a Compose rendering bug; it is a state‑hoisting problem.
Recognizable condition
You can reproduce the failure reliably: enter data, press Home, wait for the system to reclaim the process (or force‑stop the app), then relaunch from the launcher. The UI appears with default values instead of the user's last input. The state may survive configuration changes such as rotation or dark‑mode toggle, but not process death—this distinction is the key diagnostic signal.
Common causes and diagnostic table
| Observable symptom | Likely cause | Diagnostic signal |
|---|---|---|
| State survives rotation but resets after force‑stop | Using remember instead of rememberSaveable | Value is retained across recompositions but not process recreation |
| Custom object resets silently or restore crashes | Non‑Parcelable type stored in rememberSaveable without a matching Saver | No runtime warning; the saved bundle cannot encode the type |
| ViewModel‑backed state resets after process death | Missing SavedStateHandle or inconsistent keys | State survives configuration changes but not process recreation |
| State briefly shows default, then correct value appears | Recomposition or initialization overwrites the restored value before it is read | Layout Inspector or logs show a default‑to‑restored transition |
Ordered checks
Work through the checks in order. Each step narrows the fault to a specific layer.
1. Verify the state holder API
Search your composables for remember { mutableStateOf(...) }. If the value must survive process death, it belongs in rememberSaveable. If it is only needed across recompositions—for example, a transient animation value that can reset—remember is correct.
Check: In Android Studio, use Find in Files for remember { inside UI code that displays user input. Confirm each hit is intentional.
2. Inspect the saved value type
rememberSaveable can store primitives, strings, and types that implement Parcelable or Serializable. For custom data classes, either annotate them with @Parcelize (requires the kotlin-parcelize plugin) or supply a custom Saver.
Check: Look at the type argument passed to rememberSaveable. If it is a plain data class without @Parcelize, restoration will fail silently or throw during bundle encoding.
3. Confirm ViewModel wiring
If state lives in a ViewModel, the ViewModel must receive a SavedStateHandle through its constructor. The handle reads and writes to the same saved‑instance‑state bundle that rememberSaveable uses.
Check: Open your ViewModel class. If the constructor does not accept SavedStateHandle, the ViewModel cannot participate in process restoration. Also verify that every key used with savedStateHandle.get() or set() is unique and consistent across writes.
4. Look for premature overwrites
A saved value can be correctly stored and then immediately overwritten by a default during the first recomposition after restoration.
Check: Add a temporary log statement or use the Layout Inspector to observe the initial composition. If you see the default value appear before the restored value, an initialization block or LaunchedEffect is resetting the state.
Fixes tied to findings
Fix for Check 1: Replace remember with rememberSaveable
// Before: survives recomposition only
var text by remember { mutableStateOf("") }
// After: survives process death
var text by rememberSaveable { mutableStateOf("") }
For lists or custom types, provide a Saver:
val ListSaver = listSaver<MyType, String>(save = { list -> list.map { it.toSerializedForm() } },
restore = { saved -> saved.map { MyType.fromSerializedForm(it) } })
Fix for Check 2: Make the type Bundle‑compatible
Add @Parcelize to data classes used in rememberSaveable:
@Parcelize
data class FormState(val name: String, val agreed: Boolean) : Parcelable
If you cannot use @Parcelize, implement Parcelable manually, ensuring writeToParcel and createFromParcel read and write fields in the same order. Mismatched order causes silent data loss.
Fix for Check 3: Inject SavedStateHandle and namespace keys
class MyViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
var query: String
get() = savedStateHandle.get<String>("my_screen:query") ?: ""
set(value) = savedStateHandle.set("my_screen:query", value)
}
Prefix keys with a screen or feature name to avoid collisions. In your composable, read the value into Compose state so the UI observes changes:
var query by remember { mutableStateOf(viewModel.query) }
If you expose the value as a StateFlow, collect it with collectAsState() instead.
Fix for Check 4: Guard initialization
Move default‑value assignments out of the composable body and into the ViewModel or a rememberSaveable initializer. Avoid resetting state inside LaunchedEffect(Unit) unless you are certain the saved value has already been restored.
Escalation criteria
If the state still resets after applying the fixes above, follow this escalation path:
- Validate on a physical device. Emulator process‑death simulation does not always trigger the identical restore flow. Test on a real device running API 21 or higher.
- Simulate true process death. From your development machine with USB debugging enabled, run:
This kills the background process. For a full force‑stop that also clears the activity stack, use Settings → Apps → [Your App] → Force stop on the device. Replaceadb shell am kill <package_name><package_name>with your application ID. - Check your Compose version. Complex saver behavior was less reliable before Compose BOM 2023.10.00. Verify your
build.gradleuses a BOM at or above that version. - Migrate to a single source of truth. If multiple composables independently save fragments of the same logical state, consolidate them into one ViewModel backed by
SavedStateHandle. This eliminates ordering bugs where one fragment restores before another overwrites it.
Limitations and practical verification
rememberSaveable is not a database. It is designed for small UI state—text input, scroll positions, selected tabs. Large object graphs or lists of hundreds of items may exceed the saved‑instance‑state bundle size limit, which varies by OS version. For large datasets, persist to local storage and restore asynchronously.
To verify the fix without manual device testing, write an instrumentation test using ComposeTestRule: set the value, call composeTestRule.setContent again to simulate recreation, and assert the restored value. This does not perfectly replicate process death, but it validates that the Saver correctly encodes and decodes the type.
Finally, remember that custom Parcelable implementations are order‑sensitive. A field written in one order and read in another produces silent data loss with no runtime warning. Review custom Parcelable classes carefully, or prefer @Parcelize when possible.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.