
Saving UI state with ViewModel SavedState and Dagger
Updated Dagger integration to recommend using Dagger Hilt: Dependency Injection, which greatly simplifies dependency injection in ViewModels.
Updated to Lifecycle ViewModel SavedState version 2.2.0.
Check out my sample Github repository for a complete working example of everything I discuss here.
Saved State module for ViewModel is a new library from Google that builds on top of ViewModel architecture component. It let's you restore the UI state after a process stop.
In this article, I'm going to walk you through the basic usage of this library and continue deeper into a more advanced use case of integrating it with Dagger:
- Saving UI State — The Challenge
- Introduction to Saved State module for ViewModel
- SavedState ViewModel with additional constructor arguments
- Integrating SavedState ViewModel with Dagger and Hilt
- Dagger Assisted Inject
Saving UI State — The Challenge
One of the main challenges in Android development is persisting the state of the Activity/Fragment when a configuration change occurs. Android Architecture Components features the ViewModel component to address this.
Nevertheless, a configuration change is not the only behavior that results in Activity/Fragment losing its state. The state can be lost also when the system runs out of resources and has to stop the app’s process while it’s in the background. The easiest way to reproduce such behavior is to enabled Don’t Keep Activities in Developer Options of your test device. By doing so, the Activity will be destroyed whenever you leave it. ViewModels alone do not cover this case and there’s a very good article about this by Lyla Fujiwara, which I encourage you to read.
To summarize Lyla’s article, the only way to address this case is by overriding onSaveInstanceState(Bundle)
in the Activity/Fragment and persist the state in the passed Bundle
. If what you want to persist is in the ViewModel, this requires communication between ViewModel and Activity/Fragment. One way would be to introduce getInstanceState(): Map<String, Object>
and setInstanceState(state: Map<String, Object>)
in the ViewModel for setting and getting the state to/from the ViewModel when the Activity is destroyed/recreated. A major drawback of this approach is that now the state of the ViewModel is deferred to specific lifecycle events.
Note that you shouldn’t use this method for persisting large objects (the limit is 1MB), but only the bare minimum required to reconstruct the UI to its previous state. Otherwise, you may encounter TransactionTooLargeException.
Introduction to Saved State module for ViewModel
Google has released a new library called ViewModel SavedState, which is an addition to ViewModel to support Activity/Fragment state persistence through process stop. The way it works is by passing a SavedStateHandle to the ViewModel’s constructor, which you can then use to restore/save the state.
We start by including the necessary Gradle dependencies and creating the ViewModel:
app/build.gradle
:
handle: SavedStateHandle
is just a map. You can query it for its content and store the state in it like you'd normally do with a Bundle
passed to onSaveInstanceState()
. Note that to store complex objects, you have to implement the Parcelable
interface.
Pro Tip: Kotlin’s Android Extensions has a neat feature that implements the
Parcelable
interface for you by annotating your data classes with@Parcelize
. Read more here.
To get an instance of DetailViewModel
in our Activity, we will use the new extension function viewModels
included in androidx.activity:activity-ktx
library, which returns a Lazy
delegate to access the Activity’s ViewModel:
ViewModel with constructor arguments
If you’re already using ViewModels in your project, chances are that you’re passing other arguments to the ViewModels’ constructors via your own custom ViewModelProvider.Factory
so let’s see what are the options we have in this case.
SavedState module ships with an abstract factory called AbstractSavedStateViewModelFactory. This class requires us to pass an instance of SavedStateRegistryOwner and nullable Bundle
as the default state.
Let’s add another argument, githubApi
, to the ViewModel’s constructor:
The ViewModel above calls the Github API with the ID we stored previously in SavedStateHandle
.
Once again, to get this ViewModel instance we will use viewModels
extension function and pass an implementation of AbstractSavedStateViewModelFactory
that knows how to construct this ViewModel and pass the required dependencies.
And then hook it up to the Activity:
We’re passing a new instance of DetailViewModelFactory
to viewModels
extension function. The factory takes an instance of GithubApi
and SavedStateRegistryOwner
instance as a 2nd argument, which is in this case, the Activity. This 2nd argument is very important since it ties the saved state to the current component (either Activity or Fragment). In case you’re creating a shared ViewModel
among Fragment
s, you should pass the host Activity instance here and use activityViewModels
extension function instead of viewModels
.
Note that a factory is needed for each
ViewModel
that takesSavedStateHandle
as a constructor argument.
Integrating SavedState ViewModel with Dagger and Hilt
This section talks about how to inject ViewModel factories with Dagger, which results in a lot of boilerplate code that can be completely avoided by using Dagger Hilt: Dependency Injection for Android built on Dagger to replace Dagger Android. When using Hilt, you won’t need any ViewModel factories anymore since that’s already being taken care of by Hilt. Please refer to the official guide on how to add this support. Note that you must already be using Hilt. If you’re still using plain Dagger or Dagger Android and curious to see what I mean, check out this PR that migrated from Dagger Android to Hilt. You’d be amazed how much code has been removed.
The traditional ViewModel
factory with Dagger looks like this:
In the code snippet above, we have a ViewModelProvider.Factory
implementation that is injected with a map of ViewModel
class types and their Provider
s, which will then create a new instance of the required ViewModel
by calling Provider.get()
with all the dependencies injected to the ViewModel’s constructor.
Next, we utilize Dagger’s MultiBinding feature to construct the map by binding each ViewModel
class type:
As you can see in the following code snippet, we now annotated DetailViewModel
's constructor with @Inject
so that dagger will perform constructor injection and supply the dependencies.
You may have noticed that we have a problem now.
- First, we need to extend
AbstractSavedStateViewModelFactory
and notViewModelProvider.Factory
- and second, the we have a new
ViewModel
constructor argument,SavedStateHandle
, but we only get its instance whenAbstractSavedStateViewModelFactory
calls our implementation ofcreate()
function where it then passes theSavedStateHandle
instance. With the traditional Dagger setup, we get theViewModel
instance fromProvider<ViewModel>.get()
and there’s no option to pass extra constructor parameters so this approach with Dagger is not going to work anymore. We’ve already seen earlier that the only way to solve this problem is by having aViewModel
specific factory. - Moreover,
AbstractSavedStateViewModelFactory
takesSavedStateRegistryOwner
, which if you recall, is a very important argument that associates the saved state to the Activity/Fragment. We cannot just inject whatever Activity/Fragment instance here.
Unfortunately, the traditional approach is no longer valid when using SavedState ViewModel. As mentioned previously, you have to create one Factory per ViewModel while making sure that you pass the right SavedStateRegistryOwner
instance.
Nevertheless, we can still use Dagger to inject other dependencies needed for the ViewModel
by creating 2 layers of factories — covered in the next section.
Injectable factory per ViewModel
Create an interface that each ViewModel factory will need to implement:
Now create a factory per each ViewModel and implement the interface. Make sure to make this factory injectable so that Dagger will inject all the dependencies:
Notice that we don’t annotate the ViewModel constructor with
@Inject
since we the factory will create it.
Implementing AbstractSavedStateViewModelFactory for handling multiple ViewModels
With the above generic implementation, the factory can create any ViewModel since it takes the factory interface we created earlier.
Now we hook it up to the Activity by first injecting the ViewModel
factory and then using viewModels
extension function while passing AbstractSavedStateViewModelFactory
implementation, which takes the specific ViewModel factory and the current Activity instance as a SavedStateRegistryOwner
.
Check out my sample Github repository for a complete working example of everything I discussed here. Note that master branch of this repository has already migrated to Hilt. In case you’d like to inspect the code prior to the migration, please check out this branch instead. If you liked this article, please remember to clap and follow me here on Medium and on Twitter for more articles in the future.
⚠️UPDATE v2.2.0: The article ends here. The next section was written when the library was in alpha stage and is outdated. It talks about Assisted Inject library for Dagger, which I don’t recommend to use anymore with SavedState ViewModels for the reasons I listed earlier — mainly because it’s very tricky to control what instance of
SavedStateRegistryOwner
is injected by Dagger or even impossible to inject at all if you’re usingFragmentFactory
due to cyclic dependency, so I just recommend to completely avoid it unless you’re an adventurer 😄
Helping Dagger Help You
This subtitle was adopted from Jake Wharton’s talk Helping Dagger Help You about Dagger Assisted Inject at DroidCon UK.
IMPORTANT: THIS SECTION IS OUTDATED AND IS NOT RECOMMENDED ANYMORE!
We need to assist Dagger to construct the ViewModel
by passing the instance of SavedStateHandle
we get from AbstractSavedStateVMFactory.create()
. To do that, we’ll use AssistedInject as an add-on library to Dagger. AssistedInject generates factories which we can then use to construct a map of ViewModel
types and their factories by utilizing Dagger’s MultiBinding feature as we’ve done before.
First, let’s include the Gradle dependencies:
app/build.gradle
:
With AssistedInject we can tell Dagger which constructor argument we want to assist with by annotating the argument with @Assisted
annotation and instead of using @Inject
, to inject the ViewModel
, we will use @AssistedInject
.
The next step is to make AssistedInject generate the factory for this ViewModel
. We need to create a factory interface that takes the argument(s) that we want to assist with and returns an instance of the ViewModel
.
Since we want to support many different types of ViewModel
factories, we create a generic factory interface as shown above. We then extend this interface per ViewModel
and return the type of the ViewModel
we want to return:
Note that the factory interface above must be nested inside the ViewModel class otherwise you will get a compilation error.
If we build our project now, we will have a factory named DetailViewModel_AssistedFactory
generated for our ViewModel
. Next we will need to get these generated factories into our dependency graph. AssistedInject can generate a Dagger module for us so we can then just include it in one place:
We create an empty Dagger module and annotate it with @AssistedModule
. We then build to get the factories module, AssistedInject_ViewModelAssistedFactoriesModule
, generated and include it in Module(includes = [..])
as shown above. Now, include this module in your ApplicationComponent
modules so that it’s visible everywhere in the graph.
The next step is to modify our binding to construct a map of ViewModel
types and their factories:
Notice that previously we were binding the
ViewModel
s directly to the map, but now we’re binding the ViewModel factories instead into the map.
If you recall from DetailViewModelFactory
, we have a constructor that takes two arguments: SavedStateRegistryOwner
and a nullable Bundle
. We need to supplement our ViewModelFactory
to take these two extra arguments in addition to a map of ViewModel
types and their factories.
As we can see in the code snippet above, our injected map now holds ViewModelAssistedFactory<out ViewModel>>
ies instead of Provider so that we can now call create()
and pass the SavedStateHandle
to the factory viewModelMap[modelClass]?.create(handle)
.
Dagger needs to know how to inject the two new arguments (SavedStateRegistryOwner
and a nullable Bundle
) so let’s set up the bindings for them as well:
The snippet above is intentionally written in Java due to the added static provide method. I prefer writing Dagger modules in Java instead of having a nested
@Module
annotated companion object.
The only thing left to do now is to inject ViewModelFactory
in the Activity
and get the ViewModel
:
Finally, we inject ViewModelFactory
to the Activity and pass its instance to viewModels
extension function to get our ViewModel instance with all of its dependencies injected by Dagger via the assisted factory we created.
We now have a full working set up of SavedState, ViewModel and Dagger working together.
I’ve included a sample project, which demonstrates everything discussed in this article on Github.
If you liked this article, please remember to clap and follow me here on Medium and on Twitter for more articles in the future.
References:
Special thanks to Jake Wharton for pointing me to AssistedInject.