ViewModels with Saved State, Jetpack Navigation, Data Binding and Coroutines
Since its introduction, ViewModel has become one of the most “core” Android Jetpack libraries. Based on our 2019 Developer Benchmarking data, over 40% of Android Developers have added ViewModels to their apps. If you’re not familiar with ViewModels, the reason why this is the case might not be clear: ViewModels promote better architecture by separating the data from your UI, making it easy to handle UI lifecycles while also improving testability. For a full explanation check out ViewModels: A Simple Example and the official documentation.
Because ViewModels are so fundamental, there’s been a lot of work over the past couple years to make them easier to use and easier to integrate with other libraries. In this article, I’ll go over four integrations:
- Saved State in ViewModels — ViewModel data that survives background process restart
- NavGraph with a ViewModel — ViewModels and Navigation library integration
- Using ViewModels in data-binding — Easy data-binding with ViewModels and LiveData
- viewModelScope — Kotlin Coroutines and ViewModels integration
Saved State in ViewModels : ViewModel data that survives background process restart
Added in lifecycle-viewmodel-savedstate:1.0.0-alpha01
Both Java and Kotlin
The Challenge of
When ViewModels initially launched, there was a confusing issue involving
onSaveInstanceState. Activities and fragments can be destroyed in three ways:
1. You meant to navigate away permanently: The user navigates away or explicitly closes the activity — say by pushing the back button or triggering some code that calls
finish(). The activity is permanently gone.
2. There is a configuration change: The user rotates the device or does some other configuration change. The activity needs to be immediately rebuilt.
3. The app is put in the background and its process is killed: This happens when the device is running low on memory and needs to quickly free some up. When the user navigates back to your app, the activity will need to be rebuilt.
In situations 2 and 3, you want to rebuild the activity. ViewModels have always helped handle situation 2 for you, because the ViewModel is not destroyed on a configuration change; but in situation 3, the ViewModel is destroyed as well, so you actually need to save and restore data using the
onSaveInstanceState callback in your activity. I go into this tricky distinction in much greater detail in ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders.
Saved State Module
The ViewModel Saved State module helps you handle that third situation: process death. The ViewModel no longer needs to send and receive state to and from the activity. Instead you now can handle saving and restoring data all within the ViewModel. The ViewModel can now really handle and hold all of its own data.
This is done using a SavedStateHandle, which is very similar to a Bundle; it’s a key-value map of data. This SavedStateHandle “bundle” is in the ViewModel and it survives background process death. Any data you had to save before in
onSaveInstanceState can now be saved in the SavedStateHandle. For example, the id of a user is something you might store in the SavedStateHandle.
Setting up Saved State Module
Step 1: Add the Dependency
SavedStateHandle is currently in alpha (meaning the API might change and we’re looking for feedback), and it’s a separate library. The dependency to add is:
Note, if you want to keep up with what changes happen in the library, check out the lifecycle release notes page.
Step 2: Update the call to ViewModelProvider
Next, you want to create a type of ViewModel which has a SavedStateHandle. In your activity or fragment’s
onCreate, update your call to ViewModelProvider to:
The class that creates ViewModel is a ViewModel factory and there’s a ViewModel factory that makes ViewModels with SavedStateHandles called SavedStateVMFactory. The created ViewModel will now have a SavedStateHandle, associated with the activity/fragment passed in.
Note: The upcoming alpha release of the Androidx activity and fragment libraries will launch in July. In these releases (as noted here), SavedStateVMFactory will become the default ViewModelProvider.Factory when you make a ViewModel in an activity or fragment. This means that if you’re using the newest alpha versions of Androidx activity or fragment, you will not need to add the lifecycle-viewmodel-savedstate dependency or use SavedStateVMFactory explicitly. In short, when this change happens and if you’re using the new alpha versions, you can skip steps 1 and 2, and just go to step 3 below.
Step 3: Use SaveStateHandle in ViewModel
Once you’ve done this, you can use the SavedStateHandle in your ViewModel. Here is an example of keeping a user id in the SavedStateHandle:
MyViewModeltakes in SavedStateHandle as a constructor parameter.
- Save: The
saveNewUsermethod shows an example of saving data in a SavedStateHandle. You save the key value pair of
USER_KEYand then the current
userId. As data updates in the ViewModel, it should be saved in the SavedStateHandle.
savedStateHandle.get(USER_KEY)is an example of getting the current value saved in the SaveStateHandle.
Now if either the activity is destroyed due to rotation or due to the OS killing your process to free up memory, you can be ensured the SavedStateHandle will have your data.
Usually you will use LiveData in your ViewModel. For that you can use the
SavedStateHandle.getLiveData() method. Here’s an example of replacing
getCurrentUser with a LiveData, which allows for observation:
ViewModel and Jetpack Navigation : NavGraph with a ViewModel
Added in navigation 2.1.0-alpha02
Both Java and Kotlin
The Challenge of ViewModel Sharing
Jetpack Navigation works out of the box with apps that are designed with relatively few activities — or even just one — containing multiple fragments. Some of the reasons for why we picked this architecture are covered in Ian Lake’s excellent talk Single Activity: Why, When and How. One reason in particular is that this architecture allows you to share data between different destinations by creating an activity-shared ViewModel. You create a ViewModel using the activity, and then you can get a reference to that ViewModel from any fragment that’s part of the activity:
Now imagine that we have a single activity app, and we have eight fragment destinations. Of these, four of them are a shopping checkout flow:
It’s important for these four destinations in the checkout flow to share data, like the shipping address or whether the user used a coupon code. We’ll put this information in a ViewModel, but a ViewModel associated with what? This information isn’t important to the rest of the app, but previously our only option for a shared ViewModel was to associate the ViewModel to the activity. This means that all of the eight destinations would have access to this ViewModel.
ViewModel NavGraph Integration
Navigation 2.1.0 introduces ViewModels associated to a Navigation Graph. In practice, this means you can take a collection of associated destinations, such as an onboarding flow, a login flow, or a checkout flow; put them into a nested navigation graph; and enable shared data just between those screens.
To create a nested navigation graph, you can select your screens, right click, and select Move to Nested Graph → New Graph:
In the XML view, note the id of the nested navigation graph, in this case
Once you’ve done this, you get the ViewModel using
This also works in the Java programming language, using:
Note that a nested graph is encapsulated from the rest of the navigation graph. You can navigate to a nested graph (you’ll go to the start destination of the nested graph), but you cannot navigate directly to a particular destination within the nested graph from outside of the graph. Thus they are meant for encapsulated collections of screens, like a checkout flow or a login flow.
ViewModel and Data Binding : Use your ViewModel and LiveData in Data Binding
Added in Android Studio 3.1
Both Java and Kotlin
All that LiveData boilerplate
This integration is an oldie but a goodie. ViewModels usually contain LiveData, and LiveData is meant to be observed. Usually this means adding an observer in fragment:
The Data Binding library is all about observing your data and updating the UI. By using ViewModel, LiveData and Data Binding together, you can remove the previous LiveData observation code and reference your ViewModel and LiveData straight from the layout XML.
Using Data Binding, ViewModel and LiveData
Let’s say in your XML layout you want to reference your ViewModel:
To use LiveData with Data Binding, you just need to call
binding.setLifecycleOwner(this) and then pass your ViewModel to your binding, like so:
Now in your layout, you can use your ViewModel. As seen below, I set the text to
viewmodel.name could be a String or a LiveData. If it’s a LiveData, the UI will update whenever the LiveData changes.
ViewModel and Kotlin Coroutines : viewModelScope
Added in Lifecycle 2.1.0
Coroutines on Android
Kotlin Coroutines are a new way to handle asynchronous code. Another way to handle asynchronous code is to use callbacks. Callbacks are fine, but if you’re writing complicated asynchronous code, you can end up with many levels of nested callbacks; this makes your code hard to understand. Coroutines simplify all of this, and also provide an easy way to make sure you’re not blocking the main thread. If you’re new to coroutines, there’s a great in-depth blog-post series called Coroutines on Android and the codelab Using Kotlin Coroutines in your Android App.
A simple coroutine looks like a block of code that does some work:
Here I’m only starting one coroutine, but It’s easy to start hundreds of coroutines and potentially lose track of them — if you have lost track of a coroutine and it’s running some work you intended to stop, it is known as a work leak.
To avoid work leaks you should organize your coroutines by adding them to a CoroutineScope, which is an object that keeps track of coroutines. CoroutineScopes can be cancelled; and when you cancel a scope, they cancel all the associated coroutines. Above I’m using the GlobalScope, which is, as the name implies, a CoroutineScope that is available globally. It’s generally not good practice to use the GlobalScope for the same reasons it’s generally not good to write globally accessible variables. So you’ll need to either make a scope, or get access to one. In ViewModels, this is easy if you use viewModelScope.
Often if your ViewModel is destroyed, there’s a bunch of “work” associated with the ViewModel that should be stopped as well.
For example, let’s say you’re preparing a bitmap to show on-screen. That’s an example of work you should do without blocking the main thread and work that should be stopped if you permanently navigate away from or close the screen. For work like this, you should use viewModelScope.
viewModelScope is a Kotlin extension property on the ViewModel class. It is a CoroutineScope that is cancelled once the ViewModel is destroyed (when
onCleared() is called). Thus when you’re using a ViewModel, you can start all of your coroutines using this scope.
Here’s a small example:
The excellent blogpost Easy Coroutines in Android: viewModelScope goes into a bunch more detail if you’re using Kotlin Coroutines and ViewModels. For more about coroutines and architecture components, check out the documentation and the talk Understand Kotlin Coroutines on Android.
- ViewModels handle the
onSaveInstanceStatecase with the SavedStateHandle module.
- You can scope a ViewModel to a Jetpack Navigation NavGraph for more precise and encapsulated data sharing between fragments.
- If you’re using the Data Binding library and ViewModels, you can pass your ViewModel to your binding. If you’re also using LiveData, use
- …and if you’re using Kotlin Coroutines with ViewModel, then use viewModelScope to cancel your coroutines automatically when the ViewModel is destroyed.
Many of these integrations were from direct feedback and requests from the community. If there’s a ViewModel feature or integration you’re looking for you can follow the list of feature requests and consider making your own request.
Questions about any of these features? Leave a comment! Thanks for reading!
Special thanks to Ian Lake, Yigit Boyar, Jose Alcérreca, Sean McQuillan, Jisha Abubaker, and Alex Michael Cook for their revisions and contributions.