What you’ll study
- Recreation vs process death vs the user dismissing a screen.
- What survives which event, and the three layers of state.
- Where saved state goes: the Bundle the system keeps for you.
- When the Bundle is written and when it is restored.
SavedStateHandleandrememberSaveable.Bundle,Parcel,Parcelable,Serializableand Kotlin Serialization.- The size limit, and how to test process death.
How it connects
The three events differ in one thing: does the process survive, and does the user expect to
come back? Recreation keeps the process, so memory (the ViewModel) carries state across.
Process death loses the process but the user expects to return, so only the state the system kept
for you, the Bundle, comes back. Dismissal keeps nothing. SavedStateHandle, rememberSaveable,
Fragments and Navigation all write into that one Bundle, which is why it must be small and
parcelable, and why Parcelable vs Serializable matters at all.
Read it at your level
| Level | Read | You should be able to |
|---|---|---|
| Student | The basics, the survival table, the demo | Tell recreation, process death and dismissal apart |
| Fresher | + How to test process death | Reproduce process death with adb and Recents |
| Junior | + The low level 2–3, Why it helps | Choose between SavedStateHandle, rememberSaveable and a database; stop losing form input |
| Mid | + The low level 1 and 4–6, The map | Explain the registry, the allowed types, Parcelable vs Serializable vs Kotlin Serialization, and the size limit |
| Senior | + Edge cases, cross questions | Design around save timing, navigation-argument size and singletons after restore |
The basics
There are three different ways a screen can be destroyed, and they are easy to mix up:
- Recreation (configuration change). The user rotates the phone or changes the theme. Android destroys the Activity and immediately creates a new one, in the same process.
- Process death (system-initiated). Your app is in the background and the system needs memory, so it kills the whole process. Later the user comes back from Recents, and Android recreates the screen as if nothing happened, in a brand-new process.
- The user dismisses it. They press Back to leave, swipe the app away from Recents, or you call
finish(). Nothing is expected to come back.
The second one is the trap. The user didn’t leave; they expect the half-filled form, the selected tab and the scroll position to still be there. But your process, your objects and your ViewModels are gone. Only what you saved comes back.
The high level
What survives which event
This is Google’s own comparison, extended with the Compose and field cases:
| Where the value lives | Rotation | Process death | User dismisses |
|---|---|---|---|
Activity field, remember { } |
✗ | ✗ | ✗ |
ViewModel field |
✓ | ✗ | ✗ |
rememberSaveable, SavedStateHandle, onSaveInstanceState |
✓ | ✓ | ✗ |
| Database, DataStore, files, network | ✓ | ✓ | ✓ |
So the rule of thumb is three layers:
- ViewModel for state you can rebuild but don’t want to reload on every rotation.
- Saved state (
SavedStateHandle,rememberSaveable) for the small things the user would notice missing: ids, selections, input, scroll position. - Persistent storage for real data, plus anything the user would be upset to lose even after closing the app.
Where saved state actually goes
your app process system_server
──────────────── ─────────────
SavedStateHandle ─┐
rememberSaveable ─┼─► SavedStateRegistry ─► Bundle ═Binder═► kept with
Navigation stack ─┤ (one key per │ the Activity
Fragments, Views ─┘ component) │ record, even
│ if your
restored in onCreate() ◄════════════╧══════════ process dies
When your Activity goes to the background, every component writes its state into one Bundle. The
Bundle is flattened into a Parcel and sent to the system over Binder, where it is kept outside
your process. If the process dies, the next onCreate(savedInstanceState) gets that Bundle back.
Checkpoint
Before going deeper, you should be able to say:
- Recreation keeps the process; process death doesn’t; dismissal keeps nothing.
- A ViewModel survives only recreation; saved state also survives process death; disk survives everything.
- All saved state goes into one Bundle that the system keeps for you.
Run the “what survives?” demo through all three events before reading on.
The low level
1. When the Bundle is written
onSaveInstanceState(outState)runs when the Activity may be killed later: going to the background, or before a configuration change. For apps targeting Android 9 (API 28) and higher it is always called afteronStop().- It is not called when the user dismisses the screen (Back,
finish()), because nothing will be restored. ComponentActivity.onSaveInstanceStatecallssavedStateRegistryController.performSave(outState). The registry asks every registeredSavedStateProviderfor its state and stores all of it under one key:androidx.lifecycle.BundlableSavedStateRegistry.key.- Restore happens in
onCreate():performRestore()must run before the owner is STARTED (the source checks this), so every component reads its state back before the UI is shown.
2. SavedStateHandle: a map that writes itself into the Bundle
class SearchViewModel(private val handle: SavedStateHandle) : ViewModel() {
// A StateFlow backed by the saved map
val query: StateFlow<String> = handle.getStateFlow("query", "")
fun onQuery(q: String) { handle["query"] = q }
}
- The handle is created by
createSavedStateHandle()from the factory’sCreationExtras, keyed by the ViewModel’s key. All handles of an owner are collected by one provider registered asandroidx.lifecycle.internal.SavedStateHandlesProvider. - It starts with the default arguments: the Activity’s intent extras, or the Navigation destination’s arguments. That’s why nav args are readable from the handle.
- It only accepts what a Bundle can hold. The Android allow-list in the source is: primitives and
their arrays,
String,CharSequence,Bundle,Binder,ArrayList,Parcelable,Serializable,SparseArray,Size,SizeF. Anything else throws"Can't put value with type … into saved state". - With Kotlin Serialization you can store any
@Serializabletype through a delegate:var filter by handle.saved { Filter() }.
3. rememberSaveable: the Compose side
- Each
rememberSaveablecall is keyed by its position in the composition (the composite key hash), so it comes back to the same call site, even inside list items if you give them stable keys. - The values go to a
SaveableStateRegistry, which on Android is connected to the sameSavedStateRegistry, so they end up in the same Bundle. canBeSaved()allows only Bundle-compatible values. For anything else, provide aSaver(listSaver,mapSaver), make the class@Parcelize, or userememberSerializablewith a@Serializableclass.
4. Bundle, Parcel, Parcelable, Serializable and Kotlin Serialization
These are layers, not alternatives:
your object
│ how do I turn it into bytes?
├─ Parcelable ............ writes its fields into a Parcel itself
│ (@Parcelize generates the code)
├─ java.io.Serializable .. Java serialization by reflection,
│ the bytes are then written into the Parcel
└─ @Serializable ......... Kotlin Serialization; AndroidX encodes it
straight into a SavedState (Bundle) structure
▼
Bundle a typed key → value map
▼
Parcel Binder's flat byte container
▼
Binder the transaction to system_server (1 MB buffer per process)
Parcelis Android’s IPC wire format: fast, flat, not meant for disk.Parcelableis the Android way to put an object in a Parcel: you (or@Parcelize) write each field in order, with no reflection. It’s the recommended choice for saved state.java.io.Serializableworks in a Bundle too, but uses reflection and produces larger output. Fine for a small value, slow and big for anything else.- Kotlin Serialization (
@Serializable) generates serializers at compile time and works across Kotlin Multiplatform. AndroidX can encode such objects directly into saved state (encodeToSavedState,SavedStateHandle.saved,rememberSerializable).
5. Size: the limit that crashes apps
Google’s guidance, from the Parcelables and Bundles page:
- The Binder transaction buffer is 1 MB, shared by every transaction in your process at that moment, not per Activity.
- The system holds your saved state for as long as the user can navigate back to that Activity, even after your process is killed.
- Keep saved state under 50 KB.
- From Android 7.0, going over throws
TransactionTooLargeExceptionat runtime. Before that, it only logged a warning.
So never save a list of items, a bitmap or a large response. Save the id or the query, and reload the data from your repository.
6. What else is restored, and what isn’t
- Restored: the task and back stack, every Activity’s saved Bundle, the intent each one was
started with, Fragments (the
FragmentManagerre-adds them), Views with anandroid:id(EditText text, scroll positions), Navigation’s back stack. - Not restored: singletons and
objects (they start fresh, becauseApplication.onCreate()runs again), in-memory caches, ViewModels, running coroutines, anything in acompanion object. - Only the top Activity is recreated at first. Activities below it are recreated from their Bundles when the user navigates back to them.
Why it helps
- “Crash after returning to the app: lateinit not initialized.” A singleton or static field set on an earlier screen is empty in the new process. Read what you need from saved state or storage, not from memory set by another screen.
- “The form was empty when I came back.” The input lived in a ViewModel field or
remember. Move it toSavedStateHandleorrememberSaveable. - “
TransactionTooLargeExceptionwhen the app goes to the background.” Something saved a big object: a list inSavedStateHandle, a large nav argument, a Fragment’s arguments. Save ids instead. - “Fragments duplicated after rotation.” You add the fragment in
onCreate()every time. Only add it whensavedInstanceState == null; the FragmentManager already restored it. - “Works in testing, broken in production.” Developers rarely see process death because their phones have memory to spare. Test it on purpose (below).
How to test process death
- Open the screen, change some state, and press Home so the app is in the background.
- Kill it the way the system does:
adb shell am kill <your.package>(it only kills a background process), or- Android Studio’s Terminate application button in Logcat, or
- revoke a permission in Settings, which kills the process too.
- Reopen the app from Recents, not the launcher icon.
The developer option Don’t keep activities destroys Activities but keeps the process, so it tests saved state, not the loss of singletons and ViewModels. Use it as a quick check, not as the real test.
The map
| Idea | Where it lives |
|---|---|
| Save and restore hooks | Activity.onSaveInstanceState, onCreate(savedInstanceState), onRestoreInstanceState |
| One registry for all components | SavedStateRegistry, SavedStateRegistryController.performSave/performRestore, SavedStateProvider |
| The key in the Bundle | androidx.lifecycle.BundlableSavedStateRegistry.key |
| ViewModel saved state | SavedStateHandle, createSavedStateHandle(), SavedStateHandlesProvider, getStateFlow |
| Allowed types | ACCEPTABLE_CLASSES in SavedStateHandleImpl.android.kt |
| Compose | rememberSaveable, SaveableStateRegistry, Saver, rememberSerializable |
| Kotlin Serialization | androidx.savedstate.serialization (encodeToSavedState, decodeFromSavedState), SavedStateHandle.saved |
| Wire format | Bundle, Parcel, Parcelable, @Parcelize, java.io.Serializable |
| Size limit | Binder transaction buffer, TransactionTooLargeException |
Edge cases
- Swiping from Recents is not process death. It’s the user dismissing the task: saved state is thrown away and the app starts fresh next time.
- Low-memory kill while the app is visible. Rare, but the foreground app can also be killed. Nothing is restored for a screen whose state was never saved.
SavedStateHandlewrites afteronSaveInstanceState. Values set after the state was saved are only captured at the next save. Save input as it changes, not at the last moment.- Custom
Parcelableacross apps. Don’t put custom Parcelables in intents sent to other apps; their class loader can’t read your class. - Huge navigation arguments. Nav args are saved with the back stack. Pass ids, not objects.
rememberSaveableinLazyColumnitems. Without stable itemkeys, saved state can come back on the wrong item after the list changes.- Process death during a running job. In-flight network calls and coroutines are gone. Make the operation resumable, or hand it to WorkManager.
Cross questions
What is the difference between configuration change recreation and process death?
On recreation the process stays alive, so in-memory objects like ViewModels are handed to the new Activity. On process death the process is gone, so only state the system holds for you (the saved Bundle) or state on disk comes back.
Is onSaveInstanceState() called when the user presses Back?
No. It is called when the Activity might be killed and recreated later: going to the background, or a configuration change. On Back or finish() nothing will be restored, so nothing is saved.
Where is the saved Bundle stored while my process is dead?
In the system process, attached to your Activity's record. It is sent there over Binder when the state is saved, which is why it has to be a small, parcelable Bundle.
Why does SavedStateHandle reject my data class?
It only accepts Bundle-compatible types. Make the class @Parcelize (or Serializable), or with Kotlin Serialization use handle.saved { }.
Parcelable or Serializable for saved state? Why?
Parcelable. It writes fields directly into the Parcel with no reflection, so it is faster and smaller. java.io.Serializable works but uses reflection. For Kotlin Multiplatform code, Kotlin Serialization with the AndroidX saved-state encoder is the modern option.
How big can saved state be?
Google recommends under 50 KB. The hard limit is the 1 MB Binder buffer shared by the whole process; from Android 7.0, exceeding it throws TransactionTooLargeException.
How would you test that a screen survives process death?
Background the app, kill it with adb shell am kill or Studio's Terminate button, then reopen it from Recents. "Don't keep activities" is not enough: it keeps the process, so singletons and caches survive.
After process death, does the ViewModel's init run again?
Yes. It's a new ViewModel in a new process. That's why init should read its inputs (ids, query) from SavedStateHandle and reload the data.
What is the difference between remember, rememberSaveable and a ViewModel in Compose?
remember survives recomposition only. rememberSaveable also survives recreation and process death, through the saved Bundle. A ViewModel survives recreation (not process death), and lives as long as its owner, not the composable.
Revision: key points
- Recreation (same process) is not process death (new process, user returns), and neither is dismissal (nothing returns).
- Three layers: ViewModel for rotation, saved state for process death, storage for everything.
onSaveInstanceState()runs afteronStop()for apps targeting API 28+, and never on Back orfinish().- Everything goes into one Bundle through
SavedStateRegistry, sent over Binder to the system. SavedStateHandleaccepts only Bundle types; use@Parcelizeorhandle.saved { }for your own.rememberSaveableis keyed by its position in the composition; custom types need aSaver.Parcelablewrites fields directly (fast);Serializableuses reflection; Kotlin Serialization works across platforms.- Keep saved state under 50 KB: the 1 MB Binder buffer is shared, and Android 7.0+ throws
TransactionTooLargeException. - Save ids, not lists, and reload the data after restore.
- Test with Home, then
adb shell am kill, then reopen from Recents; “Don’t keep activities” is not enough.
Then close the page and try the cross questions from memory.
The fine print
The AndroidX pieces are from the current source (October 2026); the limits and timings are from Google’s Android developer documentation. The 1 MB buffer is described as “currently” 1 MB, and device behaviour under memory pressure varies, so design for small saved state and test on real low-memory devices.
This is the last part of the series for now: why read internals, launch modes, WorkManager, ViewModel.