Introduction
Your app is running smoothly in the foreground when the system needs memory for another task. Without warning, your activity is destroyed and your carefully constructed in-memory state vanishes. Understanding the Android app lifecycle: a practical Android & Kotlin guide to activities, processes and state is essential for building resilient applications that survive system constraints and user interactions. In this guide, we'll walk through how the lifecycle actually works in production scenarios, what happens during process death, and how to preserve critical data when the system reclaims your app's memory.
Activities Can Be Destroyed Anytime
One of the hardest lessons when building Android apps is accepting that your activity can be destroyed at virtually any moment, for reasons completely outside your control. The system might kill your process to free memory for a user who just opened a camera app. The user might rotate the device. Your app might be in the background for hours. Any of these scenarios can trigger activity destruction.
The lifecycle states are familiar to most developers—onCreate, onStart, onResume, onPause, onStop, onDestroy—but the key insight is understanding that transitions between these states aren't always predictable. Your activity might go from onResume directly to onDestroy without passing through the others if the process is terminated.
Let's say you're building an e-commerce app where users browse products and add items to a cart. A user selects five items, spends 30 seconds customizing options on the detail view, then receives a phone call. Android moves your app to the background. If memory pressure spikes—perhaps because a large photo editing app launched—the system kills your entire process, including the activity holding unsaved cart modifications.
Here's how you might naively handle cart state in an activity:
class ProductDetailActivity : AppCompatActivity() {
private var cartItems = mutableListOf<CartItem>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// If process was killed, cartItems is now empty
// User's selections are lost
}
fun addToCart(item: CartItem) {
cartItems.add(item)
// This is only in memory—if process dies, it's gone
}
}The problem here is obvious: cartItems lives only in memory. Once the process dies, it's gone forever. The user returns to find their customizations disappeared, and trust erodes.
Process Death Loses In-Memory State
Process death is when the Android runtime completely terminates your app's process. Unlike configuration changes (like rotation), which destroy and recreate activities but keep the process alive, process death is a hard stop. The process exits, all heap memory is released, and when the user returns to your app, it launches from scratch.
You can trigger process death yourself by opening Developer Options, enabling "Don't keep activities", and navigating away from your app. Every time your activity leaves the foreground, it's destroyed. This simulates what happens on real devices under memory pressure.
Consider a ride-hailing app where drivers are waiting for ride requests. The driver fills in their availability window—shifts until 6 PM—then receives a text message. The system kills the driver app to make room. When they tap the app again ten minutes later, they're presented with an empty form, not their partially filled availability window. The driver has to re-enter everything, and frustration follows.
What's important to understand is that process death is completely invisible to your code. There's no callback saying "you're about to die". Your activity's onDestroy might never even be called. The process just terminates. When the user returns and the activity is recreated, onCreate receives a Bundle that will be empty unless you explicitly saved state.
The system provides a small window to save state—during transitions like onPause and onStop—but only for data that fits in the bundle. Complex objects, network responses, or large datasets cannot simply be stored in the bundle.
SavedStateHandle Restores Key Data
Modern Android development uses SavedStateHandle, a repository provided by the framework that survives process death and is passed to your ViewModel. It's designed specifically to bridge the gap between activity destruction and process death scenarios.
SavedStateHandle works by automatically saving state to disk. When your app's process is restored, the framework reconstructs the SavedStateHandle with the previously saved data, giving your ViewModel access to critical information without requiring manual bundle handling.
Here's a practical example from a SaaS booking application where users draft a meeting agenda:
class MeetingViewModel(
private val savedStateHandle: SavedStateHandle,
private val meetingRepository: MeetingRepository
) : ViewModel() {
private val _agendaItems = savedStateHandle.getStateFlow(
key = "agenda_items",
initialValue = emptyList<AgendaItem>()
)
val agendaItems: StateFlow<List<AgendaItem>> = _agendaItems
fun addAgendaPoint(point: String, duration: Int) {
val current = _agendaItems.value.toMutableList()
current.add(AgendaItem(point, duration))
// This is automatically persisted to SavedStateHandle
_agendaItems.value = current
}
fun saveMeeting(title: String) {
val agenda = _agendaItems.value
viewModelScope.launch {
// When process dies and is restored, agenda is available here
meetingRepository.createMeeting(title, agenda)
}
}
}The key advantage is that SavedStateHandle uses StateFlow, so any values you store are automatically persisted across process death. When the user's phone is slammed into a drawer and the process is killed by the system, the agenda items you collected are safe. When they return, agendaItems flows the restored data immediately.
Important constraints: SavedStateHandle is limited to small, serializable data—strings, ints, parcelables. If you need to cache large responses (like a list of travel destinations with images and reviews), use a disk-backed solution like Room or a local cache layer, and store only an identifier in SavedStateHandle.
Test Process Death in Developer Options
Many developers encounter lifecycle bugs only in production because they never test process death locally. The solution is straightforward: enable "Don't keep activities" in Developer Options and test your app's transitions.
Here's the workflow:
- Open Settings → System → Developer Options
- Enable "Don't keep activities"
- Navigate through your app normally
- Leave the foreground (press home or switch apps)
- Your activity is destroyed immediately
- Return to your app and verify state is restored
With this setting enabled, every background transition triggers activity destruction, letting you catch state-loss bugs before they reach real users. It's a high-fidelity simulation of process death under memory pressure.
Let's extend our booking example with a test scenario:
// User adds three agenda items
user_adds_item("Opening remarks", 5)
user_adds_item("Q&A", 10)
user_adds_item("Next steps", 5)
// User presses home (with "Don't keep activities" on, activity is destroyed)
press_home()
// Process is killed, system decides to restore the app
open_app_again()
// SavedStateHandle restored the items
assert_agenda_items_count(3)
assert_first_item_title("Opening remarks")Without proper state handling, this test would fail—the items would be empty after the app relaunches. With SavedStateHandle, they persist.
Common Mistakes
Storing state in companion objects: Companion objects are tied to the class loader, not the activity lifecycle. Process death clears them just as thoroughly as regular memory.
// DON'T do this
class DetailActivity : AppCompatActivity() {
companion object {
var selectedItem: Item? = null // Lost on process death
}
}Ignoring the bundle in onCreate: The savedInstanceState bundle passed to onCreate is only populated for configuration changes, not process death when using modern architecture. Use SavedStateHandle in ViewModels instead.
Storing large objects in SavedStateHandle: The framework serializes and deserializes everything. Enormous collections or Bitmap objects cause delays and crashes. Use identifiers instead and fetch full data from disk or a network cache.
Assuming your activity's lifecycle methods are always called: onDestroy might not fire during process death. Don't rely on cleanup logic there. Use onCleared() in ViewModels or lifecycle observers for guaranteed cleanup.
Key Takeaways
- Android can destroy your activity at any time, including when the process is killed by the system to free memory. This is not an edge case—it happens regularly on real devices.
- Process death is invisible to your code. You won't receive a warning, and lifecycle callbacks might be skipped entirely. The process simply terminates.
- Use
SavedStateHandlein ViewModels to persist critical, serializable state across process death. It's designed specifically for this problem and integrates seamlessly with StateFlow. - Enable "Don't keep activities" in Developer Options during development to simulate process death and catch state-loss bugs before they reach production.
- Keep state in
SavedStateHandlesmall and serializable. For large datasets, use Room, disk caches, or in-memory repositories with a fallback to reload from the network. - Never rely on companion objects, static variables, or heap-only storage for data that must survive process death. They provide no protection.






Comments
Loading comments…