Opening

I want to tell you about the single most common source of bugs in Android apps. It is not null pointer exceptions. It is not off-by-one errors. It is not even concurrency bugs. It is this: developers assuming their process is still alive. You open an app, navigate three screens deep, fill out a form, switch to another app to check something, come back — and the app is on the first screen with all your data gone. Or it crashes because something that was definitely not null is now null. This happens because Android killed your process while you were away, and when you came back, the system recreated your Activity from scratch. And the app was not ready for it. Today we are going to understand exactly when and why Android kills your process, what survives and what does not, and how to write apps that handle this correctly every time.

Why Android Kills Processes

Android is an operating system designed for devices with limited memory. It runs on phones with 4, 6, maybe 8 gigabytes of RAM. And users keep lots of apps in their recents list. If every app kept its process running indefinitely, the system would run out of memory fast. So Android makes a pragmatic decision: background processes are expendable. When the system needs memory, it kills background processes to free up RAM. There is no negotiation. There is no warning. Your process just stops existing. The system decides which processes to kill based on priority. Foreground apps — the one the user is looking at — have the highest priority and are killed last. Visible apps — like one behind a transparent dialog — are next. Background apps — ones the user has navigated away from — are low priority. Empty processes — ones with no active components — are the first to go. This is called the LRU process list, and the system works through it from lowest priority to highest when it needs memory.

Not Might — Will

I want to emphasize something. Your process will be killed. Not might be killed. Will be killed. If the user switches to a memory-intensive game, or opens the camera, or just has a lot of apps in their recents list, your background process will eventually be reclaimed. On low-memory devices, it happens within seconds of going to the background. On high-memory devices, it might take minutes or hours. But it will happen. You cannot prevent it. You cannot delay it. You should not try. This is by design. It is what makes Android able to run smoothly on devices with limited resources. Your job as a developer is not to prevent process death. Your job is to handle it gracefully. And that starts with understanding what the system gives you.

onSaveInstanceState: The System's Promise

When the system is about to kill your process — or when it might kill your process — it calls onSaveInstanceState on your Activity. This is the system's promise to you: "I might destroy your process, but I'm giving you a chance to save small amounts of data so I can restore your Activity later." You receive a Bundle, and you can write key-value pairs into it — strings, ints, parcelable objects, small amounts of data. When the user returns and the system recreates your Activity, that same Bundle is passed to your onCreate and onRestoreInstanceState. The critical word here is "small." The Bundle is serialized into a Binder transaction, which has a size limit of about one megabyte for the entire transaction. In practice, you should keep your saved state to a few kilobytes at most. Save IDs, not objects. Save a user ID, not the entire user profile. Save a search query string, not the search results.

What Is Saved Automatically

The good news is that Android saves some things automatically without you doing anything. The View system has built-in save and restore logic. EditText automatically saves its text content. ScrollView saves its scroll position. RecyclerView saves its scroll position through its LayoutManager. CheckBox saves its checked state. But — and this is important — this only works if the view has an android:id attribute. Views without IDs cannot be uniquely identified across recreation, so the system cannot restore their state. This automatic saving is built into the View class through onSaveInstanceState and onRestoreInstanceState at the View level — different from the Activity methods with the same names. Each View saves its own state into the Bundle, keyed by its ID. This is why you sometimes see state magically survive configuration changes even without writing any save logic — the Views are doing it for you.

What Is NOT Saved

Here is where the pain lives. What does not survive process death? Everything else. Your ViewModel is gone. ViewModels survive configuration changes like rotation, but they do not survive process death. When the process is killed, the ViewModel is destroyed along with everything else in memory. Your navigation back stack is gone — unless you are using a navigation library that saves it to the Bundle. Your in-memory caches are gone. Your static variables are gone. Your companion object state is gone. Your singleton instances are gone. Any data you fetched from the network and stored only in memory is gone. Any coroutine that was running is gone. The DI container and all the objects it created are gone. Everything that lived in your process's heap memory is gone, because the process no longer exists. The only things that survive are what was written to the Bundle, what is in persistent storage like a database or files, and the system's knowledge of which Activity was on top.

SavedStateHandle in ViewModel

Modern Android architecture addresses this with SavedStateHandle. When you create a ViewModel using the standard ViewModel factory, it can receive a SavedStateHandle — an object that reads from and writes to the saved state Bundle. The SavedStateHandle survives process death because its contents are serialized to the Activity's Bundle. So now your ViewModel can hold state that persists across both configuration changes and process death. You read and write to the SavedStateHandle using key-value pairs, just like a Bundle. The pattern is: save IDs and small state in SavedStateHandle, and use those IDs to refetch data from your repository when the ViewModel is recreated after process death. For example, save the user ID in SavedStateHandle, and in your ViewModel's init block, use that ID to reload the user profile from your repository. The data itself does not survive process death, but the information needed to reload it does.

rememberSaveable in Compose

If you are using Jetpack Compose, the equivalent mechanism is rememberSaveable. Regular remember stores state in the Composition and survives recomposition, but it does not survive process death. rememberSaveable stores state in the saved instance state Bundle, so it does survive. For simple types like strings, ints, and booleans, rememberSaveable works out of the box. For custom objects, you need to provide a Saver — an object that knows how to convert your custom type to and from a Bundle-compatible format. The listSaver and mapSaver helpers make this straightforward for most cases. Just like with SavedStateHandle, keep the saved data small. Save identifiers and UI state, not large data objects. If you have a screen with a search query and search results, use rememberSaveable for the query text, and re-execute the search when the composable is recreated.

Navigation State

Navigation state is one of the trickiest parts of process death. The user was three screens deep. The process is killed. They come back. Where should they be? If you are using the Jetpack Navigation component, it handles this for you — it saves the back stack to the Activity's saved state and restores it on recreation. Your user returns to the same screen they were on. But the arguments passed to each destination need to be in the saved state too, and they need to be small and serializable. If you are using a custom navigation solution, you need to save and restore the navigation state yourself. Many third-party navigation libraries handle this, but not all. And if you are manually managing fragments with fragment transactions, you need to make sure the FragmentManager saves and restores its state — which it does automatically through the Activity's onSaveInstanceState, but only if you use the standard fragment lifecycle correctly.

Testing Process Death

Here is the most important practical advice in this video: you must test process death regularly. And Android gives you a simple tool to do it. Run your app. Navigate to some screen. Enter some data. Press the home button so your app goes to the background. Then in your terminal, type adb shell am kill followed by your package name. This kills your process immediately. Now tap your app's icon or switch back to it from the recents screen. The system will recreate your Activity from the saved state Bundle. If your app shows the correct screen with the correct data, congratulations — you handle process death properly. If it crashes, or shows the wrong screen, or loses the user's data, you have a bug. And it is the most common kind of Android bug. You should do this test on every screen in your app. Make it part of your QA process. Some developers even add it to their CI pipeline using automated UI tests that kill the process mid-flow.

The Most Common Android Bug

Let me describe the pattern I see over and over in production apps. A developer creates a ViewModel. They fetch data from the network and store it in a StateFlow or LiveData. The UI observes it and displays it beautifully. They test it, it works, they ship it. But they never tested process death. A user opens the app, sees data, switches to another app, comes back an hour later. The process was killed. The Activity is recreated. The ViewModel is recreated — but its StateFlow is empty because the network fetch only happened in the original ViewModel's init block, and the trigger for that fetch — maybe a user action or a navigation event — did not happen again. So the screen shows empty, or the app crashes trying to access data that was supposed to be there. The fix is straightforward: always initialize your ViewModel's data loading based on state that is saved in SavedStateHandle. If the ViewModel is recreated after process death, the SavedStateHandle still has the user ID or query parameters, and the init block can use them to reload the data.

Recap and What is Next

Let us recap. Android kills background processes to reclaim memory. Your process will be killed — not might, will. onSaveInstanceState gives you a small Bundle to save critical state. Views with IDs save and restore their own state automatically. ViewModels do not survive process death — but SavedStateHandle does, because it is backed by the Bundle. rememberSaveable is the Compose equivalent. Navigation state is saved automatically by the Jetpack Navigation component. Test process death with adb shell am kill, and do it on every screen. The most common Android bug is assuming your process survives when it does not. This wraps up Phase 5 on Android Runtime Internals. You now understand how your app starts, how the main thread works, how views become pixels, what causes ANRs, and how process death affects everything. In the next phase, we will look beyond your app — at the network layer, and how data actually gets from a server to your phone. See you there.