Process death in Android by Example.
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,
- Might still be present 😄
- 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,
- Process death, Android system kills your application if it is required to free up the RAM.
- 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.
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
.
Option 2: Manually set the “Background process limit”(in developer options) as “No background processes”
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.
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
.