All Articles

Accessing screen arguments in ViewModels using SavedStateHandle

One of the common use cases I have noticed when using HiltViewModel’s(or ViewModel with standard Dagger setup that can inject SavedStateHandle) is figuring out how to provide runtime screen arguments. Usually the ones you send as intent extras or fragment arguments.

You can use AssistedInject to provide these arguments at runtime as a workaround as mentioned in this comment. While it works, according to the later comment in that thread, it’s not recommended, since it’s possible to leak your activity/fragment instance into your ViewModel, which is not good.

In the same comment, the author also recommends a different workaround that Dagger/Hilt team recommends. This approach uses SavedStateHandle that we are injecting, to directly read the arguments. Let’s take a look at how this workaround works.

SavedStateHandle creation

First, we need to understand how we are getting SavedStateHandle in the ViewModel.

@HiltViewModel
class MyViewModel @Inject constructor(
  savedStateHandle: SavedStateHandle
) : ViewModel(savedStateHandle)

When we are creating the ViewModel inside an Activity or Fragment, and you are not providing a default factory.

@AndroidEntryPoint
class MyActivity : AppCompatActivity() {
  // using ktx extensions to create ViewModel
  private val myViewModel: MyViewModel by viewModels()
}

Internally activity/fragment uses SavedStateViewModelFactory to create ViewModel’s which will provide SavedStateHandle as a constructor param. It basically receives the default arguments that are passed down from the screen and uses SavedStateHandle#createHandle to create the handle from the bundle.

So, that’s how we end up getting a SavedStateHandle in our ViewModel. Since it contains the same arguments bundle we get in the screen, that means we can read the arguments from the SavedStateHandle in our ViewModel.

Reading SavedStateHandle arguments

If you’re using Dagger Hilt for your DI, annotating your ViewModel with HiltViewModel will automatically provide the SavedStateHandle.

Now that we have our SavedStateHandle in our ViewModel. We can just read the arguments from it, let’s consider we have a UserDetailVieWModel that requires a user id to load the user details.

@HiltViewModel
class UserDetailViewModel @Inject constructor(
  savedStateHandle: SavedStateHandle
) : ViewModel() {
  
  private val userId = savedStateHandle.get<UUID>(USER_ID_KEY)
}

That’s it, you now have access to the user id you need to load the data.

While we can get the arguments, we are missing type safety. If you’re using safe args plugin from Navigation Component, in v2.4.0, Google has added a function that can read the arguments from a SavedStateHandle. So, you can pass the handle to that function to have some type safety when retrieving the arguments.

@HiltViewModel
class UserDetailViewModel @Inject constructor(
  savedStateHandle: SavedStateHandle
) : ViewModel() {
  
  private val args = UserDetailDestinationArgs.fromSavedStateHandle(savedStateHandle)
  private val userId = args.userId
}

Wrapping up

So, this is how you can access your screen arguments in ViewModel’s. If you’re not using Hilt, you may still have to use AssistedInject depending on the dependencies you have in the ViewModel. But if you’re using Hilt, this is a good way to get the runtime arguments. If there are any other runtime arguments you want to pass to ViewModel, you can pass them when calling a specific function or even considering providing those arguments using components.