Parameter Injection for Android ViewModels

Handling parameter injecting in Android ViewModels using...

Alex Frank

September 6, 2022

2

min read

Injecting dependencies into our ViewModel is already good practice. It keeps the implementation flexible and easy to test.

But what about parameters provided to the screen or Fragment? For example, Fragment Args or Compose navigation parameters. Often something like an init method is used to receive the parameters from the View and set up the ViewModel. This adds extra steps to our ViewModel we need to be aware of. Therefore it would be more favourable to get the dependencies and the parameters in the constructor.

Setup

For this example, let’s keep it simple and focus mainly on handling the parameter.

We create an app with two screens:

  1. Screen 1 is just a button. Tapping it gets a random number and navigates to Screen 2, handing the random number over as a parameter.
  1. Screen 2 receives the random number, creates a View state, and simply displays the result as a text.

The screens are created with Jetpack Compose, and the example also uses Composes NavHost to navigate, but the same ViewModel code applies to Activities and Fragments. The only difference is the types allowed to be used as parameters. In the following setup, Compose Navigation only allows us to pass parameters as part of the navigation route String.

As we can see, our second screen has the route details/{randomNumber} declaring the parameter randomNumber.

Handle a Saved State

Now to the important question. How can we retrieve the parameter in our ViewModel on the second screen after navigation?

The SavedStateHandle class contains the information we need, and it is directly injectable into the constructor of a ViewModel.

class DetailsFlowViewModel(    savedStateHandle: SavedStateHandle) : ViewModel() {    ...}

This is possible with or without the help of a dependency injection framework like Hilt.

SavedStateHandle provides us with two methods to get to our parameter.

operator fun <T> get(key: String): T?fun <T> getStateFlow(key: String, initialValue: T): StateFlow<T>

Depending on what we want to achieve, we can use either method. In our case, we want to offer a View state flow from our ViewModel to the UI. Therefore, let’s use getStateFlow.

Important: Since we are using Compose Navigation we first have to retrieve the parameter as a String before we can convert it to its actual type Int. With Fragment Args it would be possible to directly get the parameter as an Int.

One Step Further

We can already provide our parameter directly to the ViewModels constructor. But there is still a drawback: The ViewModel constructor does not tell us exactly what it wants, but, e.g., in tests, we need to know to set randomNumber of type String to the SavedStateHandle before passing it to the constructor. Sounds like it requires a lot of knowledge of implementation details.

Wouldn’t it be better if the constructor just tells us: I want to have the parameter, randomNumber, of type Int.

We can achieve this with the help of dependency injection frameworks like Hilt.