Accessing screen arguments in ViewModels using SavedStateHandle
One of the common use cases I have noticed when using HiltViewModel
’s(or even normal ViewModel with Dagger) 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 recommend, 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. Which is using 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.
Navigation safe args & type safety
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.