This is Day 8 of my 100 Days to Senior Android Engineer series. Week 2 theme: Process & Memory. Each post: what I thought I knew → what I actually learned → interview implications.
🔍 The concept
Your app is running. The user presses Home. Five minutes later, they tap your icon again.
From the user's perspective, nothing happened. They just switched apps briefly.
From Android's perspective: your process may have been killed and restarted from scratch. Your Application.onCreate() ran again. Every singleton was re-initialized. Every in-memory state is gone.
And if your app doesn't handle this correctly, the user walks into a broken screen — a detail page with no data, a checkout flow mid-step with no cart, or worst of all, a crash.
Process death is the bug that never reproduces on a developer's machine.
💡 What I thought I knew
I knew process death existed. I knew onSaveInstanceState was the mechanism for handling it. I knew ViewModel survives configuration changes but not process death.
What I hadn't internalized was how frequently it actually happens and how many common patterns silently break under it.
😳 What I actually learned
Android's process priority hierarchy
Android kills processes based on a priority system. Understanding this explains when your process is at risk:
Priority 1 — Foreground process
Active Activity (onResume), running ForegroundService,
BroadcastReceiver executing onReceive()
→ Almost never killed. System is critically low if this happens.
Priority 2 — Visible process
Activity visible but not focused (onPause),
Service bound to a visible/foreground Activity
→ Killed only if Priority 1 processes need resources.
Priority 3 — Service process
Service running in background (startService, not foreground)
→ Killed when Priority 1 and 2 need more resources. Common.
Priority 4 — Cached process (background)
App in background, no active components
→ Killed freely. This is where most apps live after Home press.
→ Killed in LRU order — least recently used first.
Priority 5 — Empty process
No active components, kept alive for startup performance
→ First to be killed.
The moment your user presses Home and your app has no foreground service running, you drop to Priority 4. On a device with 4GB RAM and 30 apps installed, your process might survive for hours. On a device with 2GB RAM and aggressive battery optimization (common in Southeast Asian markets), it might survive for 30 seconds.
What "process death" actually looks like
When Android kills your process:
Normal flow (no process death):
User presses Home
→ onPause() → onStop() ← callbacks fire
→ Process stays in memory
User returns
→ onRestart() → onStart() → onResume()
→ Same objects, same state
Process death flow:
User presses Home
→ onPause() → onStop() ← callbacks still fire
→ [some time passes]
→ Android kills the process ← no callbacks, no warning
User returns
→ NEW process starts
→ Application.onCreate() ← fresh start
→ Activity.onCreate(bundle) ← bundle from onSaveInstanceState
→ UI restored from bundle
→ But: ViewModel is GONE, singletons are GONE, in-memory state is GONE
The key insight: the Activity back stack survives, but your app's memory does not.
Android keeps the back stack metadata. It knows you were on the detail screen. It creates a new process, creates a new Activity, and passes the saved Bundle — but everything your app was holding in memory is gone.
The specific patterns that break silently
Pattern 1: ViewModel loading data only on first creation
class OrderDetailViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
// ❌ Loaded once, never reloaded after process death
private val orderId = savedStateHandle.get<String>("order_id")
init {
// This runs on first creation AND after process death recreation
// So this is actually fine — but many devs write it wrong:
loadOrder(orderId)
}
}
// The broken version:
class OrderDetailViewModel : ViewModel() {
private var hasLoaded = false // 💀 this flag is reset after process death
fun loadIfNeeded(orderId: String) {
if (hasLoaded) return // After process death, hasLoaded = false
hasLoaded = true // so this check doesn't save you
loadOrder(orderId)
}
}
After process death, hasLoaded starts as false again — the check is meaningless. The init block approach is safer because it always runs on ViewModel creation, which happens both on first launch and after process death.
Pattern 2: Passing objects through ViewModel without persistence
// ❌ User object fetched once, stored only in memory
class ProfileViewModel : ViewModel() {
private val _user = MutableStateFlow<User?>(null)
fun setUser(user: User) {
_user.value = user // Gone after process death
}
}
// Activity passes user from login:
viewModel.setUser(loggedInUser)
// Navigate to ProfileActivity → SettingsActivity → EditActivity
// User presses Home, process killed
// User returns to EditActivity
// EditActivity.onCreate() called, ViewModel recreated, _user = null
// 💀 EditActivity tries to show user data that doesn't exist
The fix: persist what matters. Either through SavedStateHandle for lightweight data, or by re-fetching from the repository (which reads from local database or cache) on every ViewModel creation.
class ProfileViewModel(
savedStateHandle: SavedStateHandle,
private val userRepository: UserRepository
) : ViewModel() {
// The userId comes from navigation args — survives process death via Bundle
private val userId: String = savedStateHandle["user_id"]
?: throw IllegalStateException("user_id required")
// Always load from repository — handles both first launch and process death
val user: StateFlow<User?> = userRepository.getUser(userId)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
}
Pattern 3: Navigation state assumptions
// ❌ Assuming in-memory state is valid when a screen is shown
class CheckoutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Cart is held by a singleton — reset after process death
val cartItems = CartManager.instance.items
if (cartItems.isEmpty()) {
// This can happen even if the user was mid-checkout
// because CartManager was re-created empty
navigateToHome() // User loses their cart progress
}
}
}
Singletons are re-initialized after process death. CartManager.instance.items will be empty. The checkout screen defensively redirects to Home, losing the user's cart.
The fix: persist cart state to local storage. CartManager should restore from database on initialization, not start empty.
How to test for process death
This is the part most developers skip — because process death never happens during development.
Method 1: Android Studio
1. Run your app
2. Navigate to a deep screen
3. Press Home
4. In Android Studio: Run → Terminate Application (the red square)
OR use: adb shell am kill <your.package.name>
5. Tap the app icon in the Recents screen (not the launcher icon)
6. Observe: does the screen restore correctly?
The difference between step 5 and 6 is crucial. Tapping the launcher icon always starts fresh. Tapping from Recents triggers the process death restoration path.
Method 2: Don't keep activities (developer option)
Settings → Developer Options → Don't keep activities → ON
With this enabled, every time you leave an Activity (even just switching apps briefly), it's destroyed. This is an aggressive simulation — more aggressive than real process death — but it catches most of the same bugs.
Method 3: adb command for precise control
# Kill the process while keeping the task alive
adb shell am kill com.your.package.name
# Then tap from Recents — triggers full process death restoration
The correct mental model
Think of your app in two layers:
Layer 1: Process memory (volatile)
ViewModels, singletons, in-memory caches,
static fields, Application-level state
→ Lives as long as the process
→ Completely reset on process death
Layer 2: Persistent state (durable)
onSaveInstanceState Bundle (UI state)
SavedStateHandle (ViewModel state)
Room/DataStore/SharedPreferences (app data)
→ Survives process death
→ What the OS uses to reconstruct your UI
For every piece of state in your app, ask: which layer does this live in? If it lives in Layer 1 but the UI depends on it being there when the user returns, you have a process death bug waiting to happen.
🧪 The decision framework
State needs to survive...
Configuration change only (rotation)?
→ ViewModel (no SavedStateHandle needed)
Configuration change + process death?
→ SavedStateHandle in ViewModel
→ Keep it small: IDs, selections, scroll position
→ Not full objects — just enough to re-fetch or reconstruct
App session (cart, auth token, user preferences)?
→ Local database (Room) or DataStore
→ Re-load into ViewModel on creation
Nothing — ephemeral UI state?
→ Let it reset. Not everything needs to survive.
❓ The interview questions
Question 1 — Mid-level:
"What's the difference between ViewModel surviving rotation and process death?"
ViewModel is stored in the ViewModelStore, which is retained across configuration changes by the NonConfigurationInstance mechanism. But when the process is killed, the ViewModelStore is destroyed along with everything else in memory. A fresh process creates a fresh ViewModel.
SavedStateHandle bridges this gap — it's backed by the same Bundle as onSaveInstanceState, so it survives process death and is automatically restored into the new ViewModel instance.
Question 2 — Senior:
"Your app has a multi-step checkout flow: Cart → Shipping → Payment → Confirmation. How do you design state management to handle process death at any step?"
// Each step's ViewModel reads from a shared CheckoutRepository
// which persists state to Room
class ShippingViewModel(
savedStateHandle: SavedStateHandle,
private val checkoutRepo: CheckoutRepository
) : ViewModel() {
// Checkout ID from nav args — survives process death via Bundle
private val checkoutId: String = savedStateHandle["checkout_id"]!!
// Always loaded from repository — not stored in memory only
val checkoutState: StateFlow<CheckoutState> = checkoutRepo
.getCheckout(checkoutId)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), CheckoutState.Loading)
fun updateShippingAddress(address: Address) {
viewModelScope.launch {
// Persist immediately — survives process death
checkoutRepo.updateShipping(checkoutId, address)
}
}
}
The key design decision: persist state to the database immediately on user action, not on screen exit. Waiting for onPause() to persist is risky — it can be skipped. Writing to Room on every meaningful action is safe and gives you process death resilience for free.
Question 3 — Senior:
"How does Android decide which processes to kill, and how does this affect your architecture decisions?"
The LRU cache + priority system means the most recently used apps survive longest. Apps with active foreground services are essentially immune. Background apps on low-memory devices can be killed within minutes.
Architecture implications:
Don't rely on in-memory state surviving background time — especially for apps targeting emerging markets where low-RAM devices are common.
Foreground services are the right tool for truly uninterruptible work — not a performance hack. Music playback, navigation, active file uploads should be foreground services because they actively communicate to the user that the app is doing something.
Repository should always be the source of truth — not the ViewModel. The ViewModel is a UI-layer cache. The Repository is where durability lives.
Test on low-end devices — process death behavior that never happens on a Pixel 8 happens constantly on a device with 3GB RAM and aggressive vendor battery optimization.
What surprised me revisiting this
How frequently process death happens on real devices — especially in Southeast Asian markets where mid-range devices dominate and battery optimization is aggressive. I'd been testing primarily on high-end devices and missing a whole class of user-reported bugs.
The
Don't keep activitiesdeveloper option being more useful than I gave it credit for. I used to think it was too aggressive to be realistic. Now I run with it on regularly during development.Writing to the database immediately on user action, not on exit — I'd been conflating "persist on pause" with "persist on change". The former is fragile; the latter is robust. A small but important mental shift.
Tomorrow
Day 9 → onSaveInstanceState vs ViewModel vs SavedStateHandle — three mechanisms for preserving state, each with different guarantees. Which one to reach for and when.
Have you ever gotten a bug report that was caused by process death but didn't know it at the time? I'd genuinely like to know — drop it in the comments.
← Day 7: Week 1 Recap — 6 Android Fundamentals I Thought I Knew Cold
Top comments (0)