Jetpack Saved State for ViewModel: Getting Started
In this tutorial, you’ll learn how to used the Saved State library from Android Jetpack to preserve the UI state of an Android application. By Ishan Khanna.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Jetpack Saved State for ViewModel: Getting Started
15 mins
- Getting Started
- What is ADB?
- Verifying Access to ADB
- Saving UI States: The Past
- Understanding Problems with onSaveInstanceState()
- Saving UI States: The Present
- Understanding User Expectations and System Behavior
- Simulating Process Death
- Finding the App Process
- Killing the App Process
- Feeling the User Experience
- Handling The Process Death
- Adding the Saved State Library
- Retrieving The ViewModel State With SavedStateHandle
- Saving The ViewModel State With SavedStateHandle
- Instantiating ViewModel With Activity
- Gluing it All Together
- Diving Into Different Types of Data for Persistence
- Identifying Candidates for Using Saved State
- Where to Go From Here?
If your app loses user data when the user kills it or switches screens, you’re going to get bad reviews and ratings. To ensure a good experience, you must preserve the footprint, or state, of the app at any point of time. You’ll improve the user’s experience and earn good reviews.
In this tutorial, you’ll develop an app, HopelessHubby, which helps you curate a grocery list. You’ll also learn how to use the Saved State library (part of the Android Jetpack collection of libraries) to preserve your UI state.
By the end of this tutorial, you’ll learn:
- About UI State and its importance in Android apps.
- How to simulate system initiated process death.
- How to implement Saved State for ViewModels.
- What data types are available for persistence.
Getting Started
You can download the project materials by clicking the Download Materials button at the top or bottom of the tutorial. Then, open the project in Android Studio 3.4 or higher.
You’ll use some ADB commands in this tutorial. If you’re already familiar with them, feel free to skip to the Saving the UI States section below.
What is ADB?
ADB refers to the Android Debug Bridge. It’s a command-line tool that lets you communicate with a device running Android. It’s part of the Android SDK Platform-Tools package.
The adb command lets you perform various actions on a device, such as installing apps and debugging them over USB or WiFi. Since Android is a UNIX-BASED operating system, these adb commands also bring access to a Unix shell running on the device, which lets you run many commands.
If you are interested in learning more about the ADB. Checkout this article on our website.
In order to successfully completely this tutorial you need command line access to the ADB, the next section will guide you on how to verify that.
Verifying Access to ADB
Type adb devices
in your terminal and press Enter.
The output should start with the text 'List of devices attached'
.
If you see that, you’ve access to ADB via the command line.
If you’re interested in other ways to get ADB working on your machine, check out this StackOverflow answer.
Saving UI States: The Past
As the operating systems and hardware capabilities of mobile phones have evolved over the years. The responsibility of saving the states has shifted from users to the applications. Remember how you’d always hit Ctrl + S
about ten times after each changed you made in any application on your computer.
On mobile, however, it is rare that you are explicitly asked to hit a save button before you close an app or move on to another one.
Creators of Android probably envisioned this paradigm shift even before launching the first version and hence allowed developers to cater to this scenario.
This is the reason onSaveInstanceState()
method exists in Android SDK since API level 1. Through this method, the platform allows you to store any information related to the instance of the current activity that you might need when restoring it. Because no code is perfect developers often have to wrap their heads around the problems and strange behaviors this API brings along. Let’s see a couple of them.
Understanding Problems with onSaveInstanceState()
On Android platform versions lower than Build.VERSION_CODES.P there are no guarantees about whether this method will be called before or after onPause()
. What is known is that it will be called before onStop()
of the Activity’s lifecycle.
On the platforms target version Build.VERSION_CODES.P or higher, however, this method is guaranteed to be called after onStop()
of the Activity is called. These ambiguous behaviors are often hard to predict and majority of developers won’t learn about them until they encounter a bug in their app as this information is buried deep inside the documentation.
Another case when onSaveInstanceState()
is not called is when the user explicitly closes the Activity or other cases when finish()
is called.
Even with some of these caveats this API has survived the test of time and millions of developers still continue to use it.
The Android ecosystem however has evolved at a much faster pace.
With introduction of modern architectures the developers have started separating the business logic from the view itself which means views don’t know how to persist data anymore.
While this is great for the overall health of your codebase, it might affect your user experience if you don’t save the UI states whereever needed.
Although with the modern architectures available today, you can save some state information using this method, the Jetpack library is making it easier for you to do it via the ViewModel itself. In the next section, let us look at the need for saving state and what effects it has on the user experience.
Saving UI States: The Present
If you want to ensure a great user experience, your app needs to preserve and restore its UI state when it experiences system-initiated destruction.
A simple way of balancing the user expectations and system behavior is to 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.
No matter which approach you choose, you should always strive to meet the users’ expectations and provide a better user experience. Your users shouldn’t perceive any delay while loading data onto the UI, especially when a configuration change occurs. For example, they shouldn’t notice any delay when rotating their device from portrait to landscape mode.
Generally speaking, you’ll probably use both ViewModel
and onSaveInstanceState()
.
Understanding User Expectations and System Behavior
Users take various actions as they run an app. For almost every explicit action, users expect feedback or an output.
Think of output in terms of app state. The expectation could be to either clear or preserve it. In some cases, the action the user expects is automatically performed but in others it isn’t.
You can dismiss a UI state in two ways:
-
User-initiated dismissal can occur by tapping the Back button, swiping the activity off of the Overview screen, killing the app, completing any Activity through
Activity.finish()
, or simply navigating up from the Activity. - System-initiated dismissal can occur from a configuration change, such as changing to multi-window mode or a screen rotation.
Simulating Process Death
For this step, you must use an emulator with Android version P or above.
Finding the App Process
First, make sure the app is running. If it isn’t, click the Run button on top of Android Studio. Then, in your terminal type the following command and hit Enter.
adb shell ps -A
This executes a process status shell command on your device and prints a list of processes running. You added -A to list all processes.
It’s crazy to search through the list of all these processes. So, here’s a neat trick to help you do this effortlessly.
grep
a part of your app’s Package Name and it’ll narrow down the results you have to skim through.
For example:
adb shell ps -A | grep hopelesshubby
The output of this command confirms your app’s process is running and should look something like this:
In the next section, you’ll simulate the app-killing process.
Killing the App Process
First, press the Home button on your device or emulator. Then run this command in your terminal:
adb shell am kill com.raywenderlich.hopelesshubby
Next, confirm that you successfully killed the process. Copy, paste and run this command:
$ adb shell ps -A | grep hopelesshubby
You should get nothing as a result. This means you killed the process correctly.
Feeling the User Experience
So far, you’ve learned how to find running processes and kill them using the ADB. Next, you’ll run the app again to see how this disrupts user experience in real life.
To get a feel for how this affects a user, run the app again. You can do this from the device by clicking on the app icon or from Android Studio by pressing the Run Button.
Next, add a couple of items to the shopping list.
First, click on the EditText
and then add the name of your item. Then, click the ADD button.
The items should appear on the list below, like this:
Next, click the Home button and run this command in your terminal:
adb shell am kill com.raywenderlich.hopelesshubby
After the app starts, do you notice the items you entered are gone?
Frustrating, isn’t it? That’s what happens to your users as well. So, how about implementing a solution to this problem?
Handling The Process Death
By using the onSaveInstanceState()
callback you can handle the system-initiated process death gracefully and provide your users a delightful experience.
If the system kills and later recreates an Activity or a Fragment, then the callback above comes to the rescue by persisting the data needed to restore the UI State.
You’ll solve this problem in the next section by introducing the Saved State library from Android Jetpack.
Adding the Saved State Library
Time to add the ViewModel Saved State Library into your app.
First, open build.gradle in your app module.
Next, add this line to your dependencies lists:
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha02'
With the dependency added, next, you will add the code needed to load a ViewModel
‘s state.
Retrieving The ViewModel State With SavedStateHandle
Next, open ShoppingListViewModel.kt and modify its constructor by adding the following object to it:
val savedStateHandle: SavedStateHandle
The SavedStateHandle
you’ve added persists data using a key-value type interface, similar to using a Bundle or a Map. Therefore, you need to define a key with which you can save and restore the data. Add the following key to your ShoppingListViewModel
class:
private val KEY = "Saved_Shopping_List"
Next, you need to initialize your items LiveData like this:
val items : LiveData<ArrayList<String>> = savedStateHandle.getLiveData(KEY, arrayListOf())
This code makes sure the list of items comes from the persisted data if there is any. However, you haven’t yet saved any data, you’ll do that next.
Saving The ViewModel State With SavedStateHandle
A good time to save items would be when they’re added to the list.
Add the following function to ShoppingListViewModel.kt
fun addItemToShoppingList(itemName: String) {
items.value!!.add(itemName)
savedStateHandle.set(KEY, items.value)
}
This code saves the entire list of items using the savedStateHandle
each time an item is added. In this case, the list of items is an array of Strings, which is supported by SavedStateHandle
. You can persist more complex objects as long as they implement the Parcelable
interface.
Now that ShoppingListViewModel
requires a SavedStateHandle
, you’ll have to change the way it’s currently instantiated. You’ll tackle that next.
Instantiating ViewModel With Activity
Next, open ShoppingListActivity.kt and replace this line in the onCreate()
method:
viewModel = ViewModelProviders.of(this).get(ShoppingListViewModel::class.java)
with:
viewModel = ViewModelProviders.of(this, SavedStateViewModelFactory(this)).get(ShoppingListViewModel::class.java)
Here you added a SavedStateViewModelFactory
that handles the system initiated death and preserves the state of the data that the view model holds at that point of time.
Next, start fetching items using the new getter you implemented.
In onCreate()
change the way you refer to the ViewModel
‘s items like this:
val position = viewModel.items.value?.size
There’s one more step before it starts working as expected.
Gluing it All Together
Replace your loadShoppingLists()
method with this code:
// 1
private fun loadShoppingLists() {
// 2
viewModel.loadShoppingList()
// 3
viewModel.getItems().observe(this, Observer<ArrayList<String>> {
shoppingListAdapter = ShoppingListAdapter(it ?: arrayListOf())
recyclerView.adapter = shoppingListAdapter
shoppingListAdapter.notifyDataSetChanged()
})
}
- This function makes sure you get the updated shopping list and is called from
onResume
of the ShoppingListActivity. - You call
loadShoppingList
which helps you set the data in your list with the updated list, if your list is empty. - Here, you observe the changes in the list and notify the
recyclerView
.
Now it’s time to test the changes you made to the code. Build and run the app. Try adding some items to the list.
Next, press the Home button to send the app to the background. Then run this command in your terminal:
$ adb shell am kill com.raywenderlich.hopelesshubby
Next, try launching the app on the device again. You’ll see the items you added to the list persist automatically.
Voila, you did it! You made the lives of some hopeless hubbies a little better. Pat yourself on the back. :]
Diving Into Different Types of Data for Persistence
SavedStateHandle class offers the methods you expect for a key-value map:
-
get(String key)
. -
contains(String key)
. -
remove(String key)
. -
set(String key, T value)
. -
keys()
.
It accepts all the primitive types and their respective arrays in addition to Parcelable, Serializable, SparseArray, Bundle, ArrayList and Binder. For an exhaustive list, check this page.
Identifying Candidates for Using Saved State
With great power, comes great responsibility. You’ve learned a neat way to persist data for UI state, but you shouldn’t use it everywhere. Sometimes a Room Database or SharedPreferences might work better.
Before you use the SavedStateHandle, ask yourself if it is something you need. They are not a replacement for SharedPreferences or any other modes of data persistence mechanisms.
Where to Go From Here?
You can download the completed project by clicking the Download Materials button at the top or bottom of this tutorial.
ADB has other cool features you should be familiar with. If you’d like to read more about ADB, checkout the Official Google Documentation.
Want to learn more about saving UI states? This article from Google is a great place to start.
I hope you enjoyed this tutorial. If you have any questions or comments, please join the forum below!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more