Process Death - Part 2: Minimal Example Apps
Introduction
This is part of a short series of posts about Process Death and how to mange it in the Modern Android Compose environment. I strongly recommend reading these in order.
- Process Death - Part 1: Introduction and Overview
- An overview and a short code example to demonstrate
- Process Death - Part 2: Minimal Example Apps
- Examples of remember, not saving, crashing, and restoring values after Process Death
- Process Death - Part 3 - TBA but likely when and how to save more complex states
Example App 1: Remember vs. RememberSavable
Here's a tiny app with two counters you increment at the same time. Here's a full link to the code: Git Hub Gist Link
It will increment both the rememberCounter and the rememberSavable counter. If leave the app and come back, both of these should remain the same unless you hit Process Death.
Process Death will throw these out of sync (see Part 1 if you are unsure on how to cause this).
This basic demonstration shows that rememberSavable should be used to store state changes that need to survive Process Death and are not being handled by a ViewModel (see later).
Code Highlights
Counters
val rememberCounter = remember { mutableIntStateOf(0) }
val rememberSavableCounter = rememberSaveable { mutableIntStateOf(0) }
val processDeathSyncError = rememberSaveable { mutableStateOf(false) }
Sync Code
LaunchedEffect(Unit) {
processDeathSyncError.value =
rememberCounter.intValue != rememberSavableCounter.intValue
}
LaunchedEffect(Unit) {
processDeathSyncError.value =
rememberCounter.intValue != rememberSavableCounter.intValue
}
Example App 2: Process Death Crash
Next up is a short example of some innocent looking code that crashes the app. Instead of making the App crash, it just gives you a short message.
Imagine you are working on an movie app. Your user selects the movie they want to watch and the app loads the player. What could go wrong?
The app uses Navigation with NavHost and a shared ViewModel to control the state.
Here's a full link to the code: Git Hub Gist Link
Below, we'll look at the app and some of the highlights.
The Screens & Sequence
Assuming we go through the steps in order. On a fresh start, we can select a movie (1-3).
Now, if we press Home or switch to another app, and if Process Death occurs, we'll be presented with our "Crash" screen when we return:
Code Highlights
Our HomeScreen takes in our detailScreenViewModel which we can use to set the movie we are watching.
@Composable
private fun HomeScreen(
navController: NavController,
detailScreenViewModel: DetailScreenViewModel
) {
// See Github Gist for full code listingButton(
{ detailScreenViewModel.setTextCode(movie) }
) {
Text(movie.toString())
}onClick = {}
navController.navigate(Screen.ViewModelDetails.route)
}
Our ViewModelDetailScreen wrongly assumes the ViewModel will hold the textCode for the movie we are watching as this is set before the user can navigate to the detail screen.
class DetailScreenViewModel() : ViewModel() {
private val _textCode = MutableStateFlow(0)
val textCode = _textCode.asStateFlow()
init { Log.v(TAG, "DS_VM Init: ${_textCode.value.toString()}") }
fun setTextCode(newCode : Int) { _textCode.value = newCode }
}
However, this will be 0 after Process Death leading to our "Crash".
@Composable
private fun ViewModelDetailScreen(
detailScreenViewModel: DetailScreenViewModel
) {
val textCode by detailScreenViewModel.textCode.collectAsState()
CenteredColumn {
Title("Detail Screen")
//if(textCode == 0) throw IllegalStateException("")
if(textCode == 0) Log.wtf(TAG,
"WARNING: This movie doesn't exist. App will crash"
)
Text(
modifier = Modifier.padding(8.dp),
text = "You are watching movie ${textCode.toString()}"
)
AnimatedVisibility(textCode == 0) {
Text("ERROR!!! Imagine we've CRASHED!!!!")
}
}
}
This is a good example of how not considering Process Death can lead to significant issues in your app.
I'll repeat the link to the full code here as I know it can be painful trying to find links sometimes: Git Hub Gist Link
Example App 3: ViewModel Process Death Handling
This simple app tracks a counter over Process Death and indicates if Process Death has occurred at any point since the initial launch.
Here's a full link to the code: Git Hub Gist Link
The UI has two counters that can be incremented separately.
Code Highlights
This example demonstrates two approaches to saving our counter. The key is how the counter is tracked.
SavedStateBackedMinimalViewModel
This counter uses the savedStateHandle so loading or saving is handled automatically.
This is the general recommended solution for most use cases. In this example, we're tracking an Int for the counter (default value 0), but we could change this for a data class to track multiple values or a more complicated UI state.
class SavedStateBackedMinimalViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val COUNTER_KEY = "SavedStateBackedMinimalViewModel savedStateCounter"
val counter = savedStateHandle.getStateFlow(
key = COUNTER_KEY,
initialValue = 0
)
fun incrementCounter() {
val currentValue = counter.value
savedStateHandle[COUNTER_KEY] = currentValue + 1
}
}
ManualSavedMinimalViewModel
The other method is using a standard MutableStateFlow and then loading any saved value when the ViewModel starts, but saving the value now has two stages. You need to update the MutableStateFlow and also save change to the savedStateHandle.
With this minimal example, there is no good reason to chose this over the getStateFlow method above; however, you may have data that is being updated very frequently and this would then allow you to chose when that state gets saved.
class ManualSavedMinimalViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val COUNTER_KEY = "ManualSavedMinimalViewModel savedStateCounter"
private val _counter = MutableStateFlow(savedStateHandle[COUNTER_KEY] ?: 0)
val counter = _counter.asStateFlow()
fun incrementCounter() {
_counter.value = _counter.value +1
savedStateHandle[COUNTER_KEY] = _counter.value
}
}
Summary
We've looked at three app that cover using rememberSavable, issues when not handling Process Death, and how to store values that survive Process Death.
Next (Work in Progress)
We'll look at handling Process Death with some more complex code examples.- Process Death - Part 3: TBA but likely when and how to save more complex states
We'll look at handling Process Death with some more complex code examples.
- Process Death - Part 3: TBA but likely when and how to save more complex states
All Example Links
App 1: ProcessDeathRemembersApp
App 2: ProcessDeathCrashMovieApp
Comments
Post a Comment