In Android, there are many ways to persist data. We have Room for databases, in-memory storage, SharedPreferences, and DataStore. One interesting method is SavedStateHandle, which serves a specific purpose: preserving UI state through process death.
The Problem: Process Death.
Let’s start with a relatable example.
You’re using Google Maps, looking for late-night restaurants for your parents who just arrived from a flight. You find a hookah lounge with surprisingly good-looking food photos. Your parents want to research it before deciding because they’ve never been, so you leave Maps to play a game while they decide.
When you return to Maps, the selected place is gone. You’re stuck searching for it again!
What happened? Process death. Android kills your app’s process when the system needs memory (like when you launch that game). When you return to Maps, the app restarts, but the detail view state is lost.
Part of developing a good Android app is understanding how memory works and how to persist UI state to ensure a smooth user experience. SavedStateHandle is the solution for this exact scenario. You may not have noticed this until maybe your QA team did extensive mobile testing. And yes, it’s easy to miss.

What Is SavedStateHandle?
SavedStateHandle is a key-value store attached to your ViewModel’s lifecycle. It’s designed for a small, relevant UI state that needs to survive:
- Process death (system-initiated app kills)
- Configuration changes (screen rotation, theme changes)
It’s NOT for:
- Complex objects or large data structures
- Long-term persistence (use Room or DataStore instead)
- User preferences (use SharedPreferences or DataStore)
How It Works
Under the hood, SavedStateHandle uses Android’s Bundle mechanism (saved instance state). When your process is killed, Android saves this bundle and restores it when the app restarts. However, this only works for process death—if the user explicitly closes the app, the state is lost.
When should you choose SavedStateHandle?
| Method | Survives Process Death | Survives App Restart | Best Use Case |
|---|---|---|---|
| SavedStateHandle | Yup | Nope | UI state (selected item, form input, navigation state) |
| Room | Yup | Yes | Complex relational data, database operations |
| SharedPreferences | Yup | Yes | Simple key-value pairs, user preferences |
| DataStore | Yup | Yes | Modern replacement for SharedPreferences, type-safe |
Let’s See It in Practice
Let’s implement the Maps scenario. When a user selects a place, we want to preserve that selection through process death.
The Composable Screen
@Composable
fun PlaceScreen(
viewModel: PlaceViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
) {
val state by viewModel.uiState.collectAsState()
if (state.selectedPlaceId == null) {
PlacesList(
onClickPlace = { id -> viewModel.onPlaceSelected(id) }
)
} else {
PlaceDetail(
placeId = state.selectedPlaceId!!,
onBack = { viewModel.clearSelection() }
)
}
}
The screen simply tracks user input and responds to state changes. The ViewModel handles the persistence logic.
The ViewModel
class PlaceViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
companion object {
private const val KEY_SELECTED_PLACE_ID = "selected_place_id"
}
val uiState: StateFlow =
savedStateHandle.getStateFlow<String?>(KEY_SELECTED_PLACE_ID, null)
.map { selectedId -> PlaceUiState(selectedPlaceId = selectedId) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = PlaceUiState(
selectedPlaceId = savedStateHandle[KEY_SELECTED_PLACE_ID]
)
)
fun onPlaceSelected(placeId: String) {
savedStateHandle[KEY_SELECTED_PLACE_ID] = placeId
}
fun clearSelection() {
savedStateHandle[KEY_SELECTED_PLACE_ID] = null
}
}
data class PlaceUiState(
val selectedPlaceId: String? = null
)
Key Points
Observing Changes
getStateFlow<T>() creates a reactive StateFlow that automatically updates when the saved state changes. This allows your UI to respond to state changes reactively.
Updating State
Simply assign a value to savedStateHandle[key] to persist it.
Clearing State
Set the value to null to clear it when the user navigates away.
ViewModel Scope
Always use viewModelScope (not GlobalScope) to ensure coroutines are canceled when the ViewModel is cleared.
Best Practices
1. Use Constants for Keys
Always define keys as companion object constants to avoid typos and enable refactoring.
companion object { private const val KEY_SELECTED_PLACE_ID = "selected_place_id" }
2. Type Safety
SavedStateHandle stores values as Any?, so be explicit with types, like this:
// Good: Explicit type savedStateHandle.getStateFlow<String?>(KEY_ID, null) // Avoid: Relying on type inference savedStateHandle.getStateFlow(KEY_ID, null) // Type unclear
3. Handle Nullability
Since SavedStateHandle can return null, always handle nullable types appropriately.
val selectedId: String? = savedStateHandle[KEY_SELECTED_PLACE_ID] if (selectedId != null) { // Handle selection }
4. Clear State When Done
Don’t leave stale data in SavedStateHandle. Clear it when the user navigates away or completes the action.
fun onBackPressed() { savedStateHandle[KEY_SELECTED_PLACE_ID] = null }
5. Use StateFlow for Reactive Updates
Prefer getStateFlow() over direct access to enable reactive UI updates.
// Good: Reactive val state = savedStateHandle.getStateFlow<String?>(KEY_ID, null) // Less ideal: One-time read val value = savedStateHandle[KEY_ID]
Summary
SavedStateHandle is perfect for small UI state that needs to survive process death but doesn’t need long-term persistence. Think of it as a “memory” for your ViewModel that persists through system-initiated process kills, ensuring users don’t lose their place when switching apps.
By understanding when and how to use SavedStateHandle, you can create Android apps that provide a seamless user experience, even when the system needs to reclaim memory—so you don’t annoy your users :)