The preservation and restoration of app state is about presenting the user with continuity in appearance and behavior after an app is placed in the background. Users expect to be able to switch from one app to another and, on returning to the original app, find it in the exact state it was in before the switch took place.
As outlined in the chapter entitled Android App and Activity Lifecycles, when the user places an app in the background, that app becomes eligible for termination by the operating system if resources become constrained. When the user attempts to return the terminated app to the foreground, Android relaunches the app in a new process. Since this is all invisible to the user, it is the app’s responsibility to restore itself to the same state it was in when it was originally placed in the background instead of presenting itself in its “initial launch” state. In the case of ViewModel-based apps, much of this behavior can be achieved using the ViewModel Saved State module.
Understanding ViewModel State Saving
As outlined in the previous chapters, the ViewModel brings many benefits to app development, including UI state restoration in the event of configuration changes such as a device rotation. To see this in action, run the ViewModelDemo app (or, if you still need to create the project, load into Android Studio the ViewModelDemo_ LiveData project from the sample code download accompanying the book).
Once running, enter a dollar value and convert it to euros. With both the dollar and euro values displayed, rotate the device or emulator and note that both values are still visible once the app has responded to the orientation change.
Unfortunately, this behavior does not extend to the termination of a background app process. With the app still running, tap the device home button to place the ViewModelDemo app in the background, then terminate it by opening the Terminal tool window and running the following command (where <package name> is the name you used when the project was created, for example, com.ebookfrenzy.viewmodeldemo):
adb shell am kill <package name>
Code language: plaintext (plaintext)
If the adb command is not found, refer to the chapter Installing Android Studio for steps to set up your Android Studio environment.
Once the app has been terminated, return to the device or emulator and select the app from the launcher (do not re-run the app from within Android Studio). Once the app appears, it will do so as if it was just launched, with the last dollar and euro values lost. From the user’s perspective, however, the app was restored from the background and should still have contained the original data. In this case, the app has failed to provide the continuity that users have come to expect from Android apps.
Implementing ViewModel State Saving
Basic ViewModel state saving is made possible through the introduction of the ViewModel Saved State library. This library extends the ViewModel class to include support for maintaining state through the termination and subsequent relaunch of a background process.
The key to saving state is the SavedStateHandle class which is used to save and restore the state of a view model instance. A SavedStateHandle object contains a key-value map that allows data values to be saved and restored by referencing corresponding keys.
To support state saving, a different kind of ViewModel subclass needs to be declared, in this case containing a constructor which can receive a SavedStateHandle instance. Once declared, ViewModel instances of this type can be created by including a SavedStateViewModelFactory object at creation time. Consider the following code excerpt from a standard ViewModel declaration:
package com.ebookfrenzy.viewmodeldemo
import androidx.lifecycle.ViewModel
import androidx.lifecycle.MutableLiveData
class MainViewModel : ViewModel() {
.
.
}
Code language: Kotlin (kotlin)
The code to create an instance of this class would likely resemble the following:
private lateinit var viewModel: MainViewModel
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
Code language: Kotlin (kotlin)
A ViewModel subclass designed to support saved state, on the other hand, would need to be declared as follows:
package com.ebookfrenzy.viewmodeldemo
import androidx.lifecycle.ViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
class MainViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
.
.
}
Code language: Kotlin (kotlin)
When instances of the above ViewModel are created, the ViewModelProvider class initializer must be passed a SavedStateViewModelFactory instance as follows:
private lateinit var viewModel: MainViewModel
val factory = SavedStateViewModelFactory(activity.application, this)
viewModel = ViewModelProvider(this, factory).get(MainViewModel::class.java)
Code language: Kotlin (kotlin)
Saving and Restoring State
An object or value can be saved from within the ViewModel by passing it through to the set() method of the SavedStateHandle instance, providing the key string by which it is to be referenced when performing a retrieval:
val NAME_KEY = "Customer Name"
savedStateHandle.set(NAME_KEY, customerName)
Code language: Kotlin (kotlin)
When used with LiveData objects, a previously saved value may be restored using the getLiveData() method of the SavedStateHandle instance, once again referencing the corresponding key as follows:
var restoredName: LiveData<String> = savedStateHandle.getLiveData(NAME_KEY)
Code language: Kotlin (kotlin)
To restore a normal (non-LiveData) object, use the SavedStateHandle get() method:
var restoredName: String? = savedStateHandle.get(NAME_KEY)
Code language: Kotlin (kotlin)
Other useful SavedStateHandle methods include the following:
- contains(String key) – Returns a boolean value indicating whether the saved state contains a value for the specified key.
- remove(String key) – Removes the value and key from the saved state. Returns the value that was removed. • keys() – Returns a String set of all keys contained within the saved state.
Adding Saved State Support to the ViewModelDemo Project
With the basics of ViewModel Saved State covered, the ViewModelDemo app can be extended to include this support. Begin by loading the ViewModelDemo_LiveData project created in An Android Studio LiveData Tutorial into Android Studio (a copy of the project is also available in the sample code download), opening the build.gradle.kts (Module :app) file and adding the Saved State library dependencies (checking, as always, if more recent library versions are available):
.
.
dependencies {
.
.
implementation ("androidx.savedstate:savedstate:1.2.1")
implementation ("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2")
.
.
}
Code language: Kotlin (kotlin)
Next, modify the MainViewModel.kt file so the constructor accepts a SavedStateHandle instance. Also, import androidx.lifecycle.SavedStateHandle, declare a key string constant and modify the result LiveData variable so that the value is now obtained from the saved state:
package com.ebookfrenzy.viewmodeldemo
import androidx.lifecycle.ViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
const val RESULT_KEY = "Euro Value"
class MainViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
private val rate = 0.74f
private var dollarText = ""
private var result: MutableLiveData<Float> =
savedStateHandle.getLiveData(RESULT_KEY)
.
.
}
Code language: Kotlin (kotlin)
Remaining within the MainViewModel.kt file, modify the setAmount() method to include code to save the result value each time a new euro amount is calculated:
fun setAmount(value: String) {
this.dollarText = value
val convertedValue = value.toFloat() * rate
result.value = convertedValue
savedStateHandle.set(RESULT_KEY, convertedValue)
}
Code language: Kotlin (kotlin)
With the changes to the ViewModel complete, open the FirstFragment.kt file and make the following alterations to include a Saved State factory instance during the ViewModel creation process:
.
.
import androidx.lifecycle.SavedStateViewModelFactory
.
.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.application?.let {
val factory = SavedStateViewModelFactory(it, this)
viewModel =
ViewModelProvider(this, factory)[MainViewModel::class.java]
}
}
.
.
}
Code language: Kotlin (kotlin)
With the screen UI populated with dollar and euro values, place the app into the background, terminate it using the adb tool, and then relaunch it from the device or emulator screen. After restarting, the previous currency amounts should still be visible in the TextView and EditText components, confirming that the state was successfully saved and restored.
Summary
A well-designed app should always present the user with the same state when brought forward from the background, regardless of whether the operating system terminated the process containing the app in the interim. When working with ViewModels, this can be achieved by taking advantage of the ViewModel Saved State module. This involves modifying the ViewModel constructor to accept a SavedStateHandle instance which, in turn, can be used to save and restore data values via a range of method calls. When the ViewModel instance is created, it must be passed a SavedStateViewModelFactory instance. Once these steps have been implemented, the app will automatically save and restore state during a background termination.