SavedStateHandle to the Rescue

Injecting persistence into ViewModels

Patrick Dattilio
May 11 · 5 min read
Image for post
Image for post
Photo by Annie Spratt on Unsplash

Imagine this scenario: You have a search screen with a query and a location. If you suddenly need to take some pictures of your cat, the system would likely kill the Activity because the camera is memory intensive, destroying both the Fragment and ViewModel holding the search input state. Switching back to the app recreates the Activity, Fragment, and ViewModel. What happens to the location that was input? It’s gone.

We ran into this exact situation and found SavedStateHandle to be an excellent solution. This is the journey of how we got there.

ViewModel Initialization

ViewModels nicely handle configuration changes, preventing duplicate network requests. Unfortunately, they do not cleanly solve the problem of ViewModel initialization. Even though we can pass in some arguments to the Fragment using safeArgs, the ViewModel has a different lifecycle from the Fragment. The Fragment does not know if the ViewModel has been initialized or has outlived the Fragment. To prevent overriding our current UI state with a raw UI state generated from the arguments, we’d need to have a “Have I been initialized?” check in the ViewModel. This doesn’t seem optimal.

Let’s look back at our example from the introduction where we would lose the search location input when you took a photo of your cat. When the ViewModel is destroyed we could save the location using onSaveInstanceState. When the Fragment is then recreated, we can restore our location from the savedInstanceState bundle. We can then pass it into the ViewModel for initialization…. Wait. We’re having the ViewModel defer it’s initialization until the Fragment passes the savedInstanceState. There has to be a better solution.

SavedStateHandle to the Rescue

Enter SavedSateHandle– new in androidx.fragment:fragment:1.2.0 or its transitive dependency androidx.activity:activity:1.1.0.

“This is a key-value map that will let you write and retrieve objects to and from the saved state. These values will persist after the process is killed by the system and remain available via the same object.”

How do we get started using SavedStateHandle? All LifecycleOwners are now also SavedStateRegisteryOwners. Activities/Fragments/etc each have one, so we can get it directly from there!

class SearchFragment: Fragment{
private val vm: SavedStateViewModel by viewModels()
}
class SavedStateViewModel(
private val state: SavedStateHandle
) : ViewModel()

This is perfect if you only have the SavedStateHandle as a dependency to your ViewModel. Often, you want more.

Making it Work With Dagger

Our ViewModel already has some dependencies injected using Dagger. As you may have guessed, there is no magic ktx by viewModelsWithSavedStateHandleAndMyDependencies method. There is a new ViewModelFactory:

public AbstractSavedStateViewModelFactory(
@NonNull SavedStateRegistryOwner owner,
@Nullable Bundle defaultArgs)

Dagger requires a ViewModelFactory to inject our dependencies. What needs to change to also inject a SavedStateHandle? Our ViewModel is not a LifecycleOwner, so we need a way of injecting the SavedStateHandle from the Fragment. However, we inject the ViewModel into the Fragment. Did we create a circular dependency? We can’t provide the SavedStateHandle in the Dagger graph: it depends on the Fragment which depends on the ViewModel which now depends on the Fragment…

That doesn’t matter because we can use AssistedInject. To provide transient dependencies to our ViewModel that takes in the SavedStateHandle and our Fragment’s SafeArg-generated data class, we can define an @AssistedInject.Factory interface. Our Fragment already had an injected ViewModel.Factory. Now, with AssistedInject, that factory has a create method.

@AssistedInject.Factory
interface Factory {
fun create(
args: SearchFragmentArgs,
savedStateHandle: SavedStateHandle
): SearchViewModel
}

We change our ViewModel’s @Inject constructor to be @AssistedInject, and we add @Assisted to each argument that depends on the Fragment.

class SearchViewModel @AssistedInject constructor(
private val searchUseCase: SearchUseCase,
@Assisted private val args: SearchFragmentArgs,
@Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel()

Our Dagger module needs two updates:

  1. Adding the @AssistedModule annotation, and
  2. Including the generated AssistedInject_YourModuleName::class.
@AssistedModule
@Module(includes = [AssistedInject_SearchModule::class])
abstract class SearchModule {
//...
}

We need one more class to tie together our new ViewModel.Factory and SavedStateHandler, the AbstractSavedStateViewModelFactory. Shout out to kakai248 who’s gist helped us put this together:

@MainThread
inline fun <reified VM : ViewModel> Fragment.activityViewModel(
noinline provider: (SavedStateHandle) -> VM) =
createLazyViewModel(
viewModelClass = VM::class,
savedStateRegistryOwnerProducer = { requireActivity() },
viewModelStoreOwnerProducer = { requireActivity() },
viewModelProvider = provider
)
fun <VM : ViewModel> createLazyViewModel(
viewModelClass: KClass<VM>,
savedStateRegistryOwnerProducer: () -> SavedStateRegistryOwner,
viewModelStoreOwnerProducer: () -> ViewModelStoreOwner,
viewModelProvider: (SavedStateHandle) -> VM
) = ViewModelLazy(
viewModelClass,
{ viewModelStoreOwnerProducer().viewModelStore }
) {
object : AbstractSavedStateViewModelFactory(savedStateRegistryOwnerProducer(), Bundle()) {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
return viewModelProvider(handle) as T
}
}
}

We pass in our Fragment arguments and the savedStateHandle. Dagger and Assisted Inject handle the rest.

private val args: SearchFragmentArgs by navArgs()private val viewModel: SearchViewModel by activityViewModel {  
viewModelFactory.create(args, it)
}

Together in Harmony

Now let’s actually use it in our ViewModel:

private val mutableUiState: MutableLiveData<SearchUiState> =
savedStateHandle.getLiveData(
“KEY”,
SearchUiState(args.initialUiState)
)

This is a LiveData of your UIState. As you make changes, update the UI, and post to this mutableLiveData, SavedStateHandle saves the updates into its map!

For a given key it can return either the value or a MutableLiveData. SavedStateHandle will subscribe to the requested MutableLiveData. This means that parcelable data you post is automatically added to its internal key-value map.

Now, if the process is killed, when the ViewModel is recreated, it’ll get the latest value from SavedStateHandle instead of the default we previously created with the arguments.

Image for post
Image for post
Image for post
Image for post
Before and After

Perks

No extra network calls are made, no confusing “was this initialized yet?” checks. Safe args make this much easier to read, understand, and avoid typos for key names.

What if you need to update another Fragment? Replace activityForResult? Pass data between Fragments? Better SafeArg integration to remove String literal keys? TO BE CONTINUED

This blog post was written in collaboration by Patrick Dattilio, Tina Siswandari(tsiswandari@gmail.com), and Victoria Gonda.

Making Meetup

We're here to make Meetup.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store