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
}

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).


Selecting movie 2, we can now select 'watch movie #'.


... and this take us to our beautiful watch screen:


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:


Basically, the ViewModel is recreated and the value for the currently watched movie is not being saved.

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 listing
                Button(
{ 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.



After process death, the values are retained. Note the Process Death notification at the bottom to ensure it is working.

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

All Example Links

Comments

Popular Posts