3 Modern Ways To Tackle Assisted Injection for ViewModels in Android

Save your states with Dagger and SavedStateHandle

Stephen Vinouze
Better Programming

--

Star Wars figurines
Photo by Daniel Cheung on Unsplash.

If you’re using Dagger as a dependency injection (DI) framework, chances are you’ve faced a situation where you couldn’t provide all dependencies.

Let’s start with a simple example. First, we want to create a manager that needs a repository to operate:

To provide the repository dependency with Dagger, you’d end up with this (assuming the Repository is provided somewhere in a Module):

Now let’s assume you need an identifier to fetch a specific resource from your repository. You’d want to pass this identifier to the manager to let your injected repository query the wanted resource:

Since Dagger cannot provide this runtime parameter, you could extract it from the constructor and manually provide it by either:

  • Using lateinit and praying the manager receives its value before using it or the application will crash:
  • Declaring the identifier nullable and dealing with nullability, although it doesn’t make sense (since the manager can’t operate without it):
  • Creating a factory with all injectable parameters then giving the runtime parameter through a create method:

Then you’d need to inject the Factory instead of the manager. Finally, you would use the create method to pass in the identifier:

Overall, all of these solutions work, but they’re showing limitations. Besides, we still have two unsolved problems:

  1. Providing a ViewModel doesn’t work as easily as the example above.
  2. We haven’t addressed how we’ll store our state within the ViewModel.

Providing a ViewModel That Saves States

As stated earlier, DI for ViewModel doesn’t work like with any other objects. Even though you could let Dagger provide your constructor parameters, you still need to use a ViewModelProvider to create one ViewModel instance.

The provider expects both an owner and a factory where you’ll pass in your injected parameters:

ViewModelProvider(owner, factory).get(MyViewModel::class.java)

The owner defines the ViewModel’s scope. Depending on it, the system will know whether to create a new ViewModel or retrieve an existing one satisfying the owner. Most of the time, it will be either an Activity or a Fragment.

The complicated part comes with the factory. When providing parameters to your ViewModel, you’ll need to pass those arguments to a custom factory that extends the androidX ViewModelProvider.Factory.

I won’t go into details, as there are many ways to do this — either manually or by leveraging Dagger multibindings.

As for saving the state, androidX gives a mechanism based on a SavedStateHandle object. It accepts a Bundle where you can put the data you want to store and retrieve directly from your ViewModel.

In our previous example, we would insert our identifier inside a Bundle and pass it to an SavedStateHandle instance. Here, I expose a static map method to create the SavedStateHandle from the identifier parameter, and I can retrieve it from within the ViewModel at any time:

You’ll need to include this dependency in your project:

implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:${latest_version}")

Assisted Injection to the Rescue

We’re trying to find a solution that lets our DI build an object with both provided and runtime parameters. Besides, we also want to save our state within the ViewModel.

After many experiments, I present to you three working alternatives along with their advantages and disadvantages.

Ultimately, choosing one solution over another will mostly be based on your project setup.

Without further ado, here come our candidates!

1. The elegant ViewModelInject library

I could have started by showing you the Assisted Injection library made by Square. Yet, Jordan Hansen went further by creating a library on top of the Square library. It offers a neat API that:

  • Generates the ViewModel factory through a custom @ViewModelInject annotation.
  • Accepts a SavedStateHandle parameter with the Square @Assisted annotation.

Pretty awesome, right? It solves all our requirements without any boilerplate. This is how our previous example would look using this library:

To make this work, you’ll need to create a Dagger module per Gradle module. And its naming matters. Let’s assume I’m creating the ViewModel within a Gradle module named awesome-module. The Dagger module must be:

Finally, you can inject the generated factory and use its create method with the intent’s bundle. The library will take care of building the SavedStateHandle for you by providing the Activity ’s bundle within intent.extras:

We could go further by lazily creating our ViewModel with a Kotlin delegate:

Then the example above becomes:

Even though this library meets our expectations, it’s worth mentioning you’ll need to depend on both this library and AssistedInject from Square. Besides, both these libraries became deprecated as soon as Dagger recently supported this DI pattern.

Finally, we noticed this library didn’t work well with incremental builds. The generated code often gets in the way and forces you to temporarily disable incremental builds within the gradle.properties file to reset the cache.

2. The newcomer Hilt

Although Hilt just appeared on our radar in 2020, this promising DI library just hit stable a few weeks ago (at the time of writing). All eyes are turned towards this new candidate.

Not only does it use the same mechanisms as Dagger, but it also removes all the hurdles when it comes to configuring your project to use DI.

As for assisted injection, Hilt supports it out of the box. All you need to do is to annotate your ViewModel class with @HiltViewModel and give a SavedStateHandle parameter in the constructor. Hilt will resolve its dependencies and provide a SavedStateHandle when creating the ViewModel. You don’t even need to inject a Factory. Hilt does everything for you!

To use the built-in Kotlin delegates, you’ll need to add the following dependencies:

implementation("androidx.activity:activity-ktx:{latest_version}")
implementation("androidx.activity:fragment-ktx:{latest_version}")

They give Kotlin delegates to both Activity and Fragment that will lazily create your ViewModel:

// From an Activity
val viewModel: MyViewModel by viewModels() // Activity scope
// From a Fragment
val viewModel: MyViewModel by viewModels() // Fragment scope
val viewModel: MyViewModel by activityViewModels() // Activity scope

All in all, assisted injection with Hilt comes down to these few lines:

On the paper, Hilt looks like it offers the most advanced and concise API for our problem. Yet, if I can give my two cents:

  1. Assisted injection for ViewModel works automagically and can be confusing for untrained eyes. It helps to know how it works under the hood. Otherwise, get prepared to struggle while debugging your application.
  2. Hilt has barely hit stable and most projects don’t use this DI library. Also, migrating to Hilt doesn’t come cheap and may not offer the same capabilities that you have with your current DI system.

3. The upgraded Dagger

Last but not least, we’re back to the good old Dagger! Since version 2.31, Dagger supports assisted injection.

It combines two annotations to tell Dagger how to inject your ViewModel. In our previous example, this was the implementation:

All @AssistedFactory interfaces must comply with two things:

  1. It must contain a single method.
  2. This method must return the ViewModel class type.

SavedStateViewModelFactory declares the generic contract so that you only need to annotate your factory interface and make it extend this interface.

As for creating the ViewModel, you’ll need to inject the factory and give it the Activity’s bundle to create the SavedStateHandle assisted object. To simplify this, we’ll create our own Kotlin delegate:

Then it all comes down to injecting the factory and giving it to our delegate to call its create method with the given SavedStateHandle:

With this, everything works with pure Dagger at the cost of a Kotlin delegate to assist you. If we compare this to Hilt, it’s more verbose since you need to:

  • Declare all assisted parameters with an @Assisted annotation.
  • Declare a factory with an @AssistedFactory annotation with all @Assisted parameters (the order matters!).
  • Inject the factory and give it to a Kotlin delegate (doesn’t work well with androidX delegates).

Having said that, you don’t need any additional third-party libraries. And you don’t get the confusion surrounding how Hilt hooks everything up.

Which Solution Should You Choose?

No matter what solution you choose, bear in mind that it always depends on your project’s constraints and what you value when it comes to using a DI solution.

I’ve used the first solution for a long time, and it satisfied me. However, I’m considering moving to the third solution, as I see no reason to migrate to Hilt as of now. If I were to start a fresh new project, I’d probably consider the second option.

--

--

✍️ Content creator | 👀 200k Views | 🤖 Keen interest in Android and Jetpack Compose | 🤝 Support me: https://medium.com/@s.vinouze/membership