Process Death - Part 1: Introduction and Overview

Introduction

This is part of a short series of posts about Process Death and how to mange it in the Modern Android  Compose environment.
  • Process Death - Part 1: Introduction and Overview
    • An overview and a short code example to demonstrate
  • Process Death - Part 2: TBA
  • Process Death - Part 3? - We'll see if this is needed (hopefully not)

What is it, and when does it occur?

Process Death means your app has been terminated by Android and no longer resides in memory, but it still appears in Recents*. If the app has implemented proper state saving, it should generally restore to a similar state when reopened.

* The button that shows recent tasks — bottom left on my Samsung phone.

The app remains listed in Recents and looks the same as always, so there is no easy way to know if Process Death has occurred. This makes testing slightly harder (see below for more information).

Process Death occurs when a device needs to reclaim memory from an app that is no longer in the foreground. This can sometimes be seen with apps in Recents that haven’t been accessed for a while. On devices with less memory or stricter task-retention policies, it may happen much sooner.

You may be able to observe this on a real device by opening Recents and selecting one of the older apps you haven’t used for a while. If it restarts instead of resuming instantly, Process Death likely occurred. If you have any of your own apps on the device, I encourage you to try this.

How can we cause it?

Rather than wait for this to happen naturally as part of the Lifecycle (see below), we can trigger Process Death manually with Developer Options.
  • From Developer Options* go most of the way down the screen (around 4/5th) until you find the Apps section and the Don't keep activities option. Select this.

* On my Google Pixel 9 emulator, after unlocking Developer Mode (About->Build Number - 5 clicks), go to System->Developer Options.

Switching from or minimizing any app will now put it into Process Death.

The Lifecycle and how to see Process Death

To detect, and deal with, Process Death, it's good to have a solid understanding of the Android Lifecycle.

One of the easiest ways to get a good feel for this is add logging to your app so you can see what is happening at any given time. Just add this to your MainActivity:


private val TAG = "Main"

override fun onCreate(savedInstanceState: Bundle?) {
val FUNC = "onCreate(savedInstanceState: Bundle?)"
Log.d(TAG, "Lifecycle: $FUNC")
super.onCreate(savedInstanceState)
//...
}

override fun onRestart() {
val FUNC = "onRestart()"
Log.d(TAG, "Lifecycle: $FUNC")
super.onRestart()
}

override fun onStart() {
val FUNC = "onStart()"
Log.d(TAG, "Lifecycle: $FUNC")
super.onStart()
}

override fun onResume() {
val FUNC = "onResume()"
Log.d(TAG, "Lifecycle: $FUNC")
super.onResume()
}

override fun onPause() {
val FUNC = "onPause()"
Log.d(TAG, "Lifecycle: $FUNC")
super.onPause()
}

override fun onSaveInstanceState(outState: Bundle) {
val FUNC = "onSaveInstanceState()"
Log.d(TAG, "Lifecycle: $FUNC")
super.onSaveInstanceState(outState)
}

override fun onStop() {
val FUNC = "onStop()"
Log.d(TAG, "Lifecycle: $FUNC")
super.onStop()
}

override fun onDestroy() {
val FUNC = "onDestroy()"
Log.d(TAG, "Lifecycle: $FUNC")
super.onDestroy()
}


Using this logging, you should see the following in these circumstances:

    First Start:
        Lifecycle: onCreate(savedInstanceState: Bundle?)
        Lifecycle: onStart()
        Lifecycle: onResume()

    Tabbing Out:
        Lifecycle: onPause()
        Lifecycle: onStop()
        Lifecycle: onSaveInstanceState()

        (with process death)
            Lifecycle: onDestroy()

    Returning to App
        (no process death):
            Lifecycle: onRestart()
            Lifecycle: onStart()
            Lifecycle: onResume()

        (after process death):
            Same as First Start

Can we ignore it?

Ignored, Process Death may cause crashes, security issues, or unexpected behavior. It may also have no effect whatsoever. It all depends on what your app is doing and how it is built; the best way to be sure is to test.

In some cases, Apps will act as if they are being started fresh. This is generally not ideal as users might expect to be taken back to where they left off: users might lose text, information, or other progress.

If you Navigation, your app will automatically save the last page the user was on and take you back there. This can be a huge issue as the ViewModel may not have the information it needs work as intended.

Code: MinimalProcessDeathDetection

The code below is the shortest example I could come up with for detecting and displaying Process Death. You can use this to test you are actually causing Process Death as expected.

ViewModel


class MinimalProcessDeathDetectionViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val PD_KEY = "KEY-PD"
private val _processDeathOccurred = MutableStateFlow(
savedStateHandle[PD_KEY] ?: false
)
val processDeathOccurred = _processDeathOccurred.asStateFlow()
init { savedStateHandle[PD_KEY] = true }
}

UI


@Composable
fun PdMinUi(viewModel: MinimalProcessDeathDetectionViewModel) {
val processDeath by viewModel.processDeathOccurred.collectAsStateWithLifecycle()
Text("Process Death Occurred: $processDeath")
}

UI Host (minimal and basic, but useful if needed)

@Composable
fun ProcessDeathMonitorMinimalApp() {
val viewModel = viewModel<MinimalProcessDeathDetectionViewModel>()
Column(Modifier.padding(vertical = 128.dp, horizontal = 64.dp)) {
Text("ProcessDeathMonitorMinimalApp")
PdMinUi(viewModel)
}
}
Very simply, the ViewModel (VM) sets _processDeathOccurred.value on launch to false if no value has been saved in the SavedStateHandle already. The VM will get recreated after process death but not if the app is resumed from the background. If recreated, it will load the value as true. If it resumes, it keeps the previously loaded false value.

Here's a full link to the code: Git Hub Gist Link

Summary

So we've gone over the basics of Process Death with a short code example that detects it.

Next

We'll look at handling Process Death with some more complex code examples.
  • Process Death - Part 2: TBA

References

The ULTIMATE Guide to Sharing Data Between Screens in Jetpack Compose - Philipp Lackner

  • Gemini Summary of Video:
The video "Sharing Data Between Screens In Android" provides a guide on five different methods to share data between screens in Android using Jetpack Compose:
  1. Navigation Arguments: A simple method for passing stateless data between one or two screens. It's easy to use and the values survive process death [01:04].
  2. Shared View Model: A more popular approach where a single view model instance is shared between multiple screens. It supports real-time state changes and is suitable for sharing data across several screens within a feature [06:03].
  3. Sharing a Stateful Dependency (Singleton): The video advises against this approach as it can be dangerous and difficult to maintain [13:29].
  4. Composition Locals: A Compose-specific way to share data within a composable tree, simplifying data sharing and avoiding prop drilling [17:12].
  5. Persistent Storage: Uses persistent storage like SharedPreferences to save and retrieve global data that needs to survive application and process death. It's ideal for data like user sessions [20:20].
The video concludes that all methods, except the Singleton approach, have a valid use case and understanding when to use each is crucial for building robust Android applications [22:57].

Processes and app lifecycle:

https://developer.android.com/guide/components/activities/activity-lifecycle

Saved State module for ViewModel 

https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-savedstate

Comments

Popular Posts