Process death in Android by Example.

9 minute read

Let’s say your app uses text input for user’s name and email on login screen. User enters their info on those fields, but they had to minimise your app and open another application before hitting the submit button. Now, two things can happen if the user comes back to your application. The input entered by the user on those fields,

  1. Might still be present 😄
  2. Might not be present 😭

The second flow is frustrating to the user. It is a bad user experience. The state wasn’t persisted on the app. It could be due to,

  1. Process death, Android system kills your application if it is required to free up the RAM.
  2. User has set the “Background process limit”(in developer options) as “No background processes”.

Let’s look at how to address this problem in this post. We’ll simulate the same on a simple login form.

Process death

The system kills processes when it needs to free up RAM; the likelihood of the system killing a given process depends on the state of the process at the time. Process state, in turn, depends on the state of the activity running in the process. Below table shows the correlation among process state, activity state, and likelihood of the system’s killing the process.

Likelihood of being killed Process state Activity state
Least Foreground (having or about to get focus) Created/Started/Resumed
More Background (lost focus) Paused
Most Background (not visible) Stopped/Destroyed

How to restore the UI state

Preserving and restoring an activity’s UI state in a timely fashion across system-initiated activity or application destruction is a crucial part of the user experience. In these cases the user expects the UI state to remain the same, but the system destroys the activity and any state stored in it.

To bridge the gap between user expectation and system behavior, use a combination of ViewModel objects, the onSaveInstanceState() method, and/or local storage to persist the UI state across such application and activity instance transitions. Deciding how to combine these options depends on the complexity of your UI data, use cases for your app, and consideration of speed of retrieval versus memory usage.

Options for preserving UI state

When the user’s expectations about UI state do not match default system behavior, you must save and restore the user’s UI state to ensure that the system-initiated destruction is transparent to the user.

Each of the options for preserving UI state vary along the following dimensions that impact the user experience:

  ViewModel Saved instance state Persistent storage
Storage location in memory serialized to disk on disk or network
Survives configuration change Yes Yes Yes
Survives system-initiated process death No Yes Yes
Data limitations complex objects are fine, but space is limited by available memory only for primitive types and simple, small objects such as String only limited by disk space or cost / time of retrieval from the network resource
Read/write time quick (memory access only) slow (requires serialization/deserialization and disk access) slow (requires disk access or network transaction)

Example

We’ll look at a simple login form and simulate process death on the same. It has two fields, name and email and looks like below.

drawing

Show me code

I have used jetpack compose to create a simple login form. It uses hilt to bind the dependencies. Showcasing the main parts of the app.

MainActivity

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // set up code..
        setContent {
            LoginScreen()
        }
    }
}

LoginScreen

@Composable
fun LoginScreen(
    viewModel: MainActivityViewModel = hiltViewModel()
) {
    LazyColumn {
        item {
            NameField(viewModel)
        }
        item {
            EmailField(viewModel)
        }
        item {
            SubmitButton()
        }
    }
}

@Composable
fun NameField(viewModel: MainActivityViewModel) {
    TextField(
        value = viewModel.nameField,
        onValueChange = { viewModel.onNameChanged(it) },
        label = { Text("Email") }
    )
}

@Composable
fun EmailField(viewModel: MainActivityViewModel) {
    TextField(
        value = viewModel.emailField,
        onValueChange = { viewModel.onEmailChange(it) },
        label = { Text("Email") }
    )
}

MainActivityViewModel

@HiltViewModel
class MainActivityViewModel @Inject constructor(
) : ViewModel() {
    var nameField by mutableStateOf("")
        private set

    var emailField by mutableStateOf("")
        private set

    fun onNameChange(updatedName: CharSequence) {
        nameField = updatedName.toString()
    }

    fun onEmailChange(updatedName: CharSequence) {
        emailField = updatedName.toString()
    }
}

Simulating process death

Option 1: Run your app and enter your data in text fields. Minimise the app and click on Terminate Application in Android studio’s Logcat.

drawing

Option 2: Manually set the “Background process limit”(in developer options) as “No background processes”

drawing

The current state of the app

Here we’ll simulate Process death by manually setting the “Background process limit”(in developer options) as “No background processes”. Then upon entering user details on name and email fields, minimise the current app and open another app (say, playstore). As you may notice, after opening the current app back, the state of name and email fields are missing.

drawing

Save state to fix the process death

We’ll use ViewModel objects’s onSaveInstanceState() method to save the state. Now the MainActivityViewModel would look like

@HiltViewModel
class MainActivityViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    var nameField by mutableStateOf(savedStateHandle.get("nameKey") ?: "")
        private set

    var emailField by mutableStateOf(savedStateHandle.get("emailKey") ?: "")
        private set

    fun onNameChange(updatedName: CharSequence) {
        nameField = updatedName.toString()
        savedStateHandle.set("nameKey", nameField)
    }

    fun onEmailChange(updatedName: CharSequence) {
        emailField = updatedName.toString()
        savedStateHandle.set("emailKey", emailField)
    }
}

As you can see in the below gif, even after we simulate Process death as before, the state of the application is retained. All thanks to SavedStateHandle.

drawing

References