How To Inject ViewModel With Dagger & What Might Go Wrong

Serge Shustoff
Wrike TechClub
Published in
5 min readAug 2, 2021

Handling ViewModel Dependency Injection is a popular topic with manuals all over the internet, but let’s check them for hidden traps and disadvantages.

The main problem is that ViewModel should be created using “ViewModelProvider(this, factory).get(YourViewModel::class.java)” at some point. It might be hidden inside a delegate “by viewModels { factory }” or be called directly. However, without it, ViewModel won’t persist through configuration changes, and its onCleared method won’t be called when it’s no longer needed.

Note: This article requires some knowledge of Dagger.

To make our examples as simple as possible, I’ll assume that we use a single component, AppComponent. Most examples could scale to architecture with a Subcomponent per view model or to a single Subcomponent for all view models.

In basic scenarios our VM will look like this:

The Repository is provided by some module in AppComponent or just has a constructor annotated with @Inject.

Also, I assume that we can easily get AppComponent from fragments with a method like:

Now let’s go over our possible approaches and their disadvantages.

1. Providers map in ViewModelProvider.Factory (with or without multibindings)

There are several methods. The easiest way is to inject view model providers into a factory and map them manually:

Add this factory to the component (AppComponent is used for simplicity):

You can then easily create your ViewModel in fragment or activity like so:

This approach has different variations with multibindings, but the core idea is the same.

Although it works well and allows us to create a VM in a few lines, it has certain limitations:

  1. It makes our factory a service locator, which means that if you forget to add mapping in the factory (or in a module if you use multibinding), then your app will crash in runtime without any indication that something was wrong during compilation. Compile-time safety is a great advantage of Dagger and we don’t want to lose it.
  2. It doesn’t allow us to pass any arguments to VM that aren’t available from DI. We can’t use assisted injection with this approach, though if all view models share the same list of parameters it would be possible, e.g., for SavedStateHandle.

2. Use Hilt

Hilt is a great new tool for Android developers that greatly simplifies some aspects of working with Dagger (and makes the learning curve even steeper). While you don’t want to pass anything from Fragment to view model it works like a charm:

And in Fragment you need only one line:

Of course for all this to work you need to set up Hilt correctly, which is covered by multiple articles and an official guide. Notice that if you forget to add @HiltViewModel, then your app will crash in runtime, not in compilation time.

But if we wanted to pass something from Fragment to view model, we would have to inject an assisted Factory into a Fragment and then create a ViewModel Factory that uses it.

In this case our view model would look like this:

We would also need some kind of universal VM factory just to avoid boilerplate factories for each VM:

Now, we can inject the Factory into a Fragment and use it for creating a VM like this:

This way we lose some advantages of Hilt and it works much like Dagger with extra steps. If you’re OK with that or don’t need an assisted injection for VM, then Hilt is a great choice.

3. Get VM from DI and pass the reference to Factory in viewModels delegate

This one won’t work, because the lambda that we passed to viewModels is invoked after configuration changes. This is rather unusual, but there are cases of a more complicated form of this solution.

viewModelComponent().myViewModel() being called after every configuration change will lead to ViewModels multiplying. If you use some resources inside or launch some coroutines in an init {} block, then those resources and coroutine context won’t be closed or cleared.

Even if this approach worked, there’s still a possibility that someone will use it with ViewModelProvider().get() directly and experience the same problems.

4. Pass lambda for creating VM to Factory

This one works fine. But there’s a hidden danger to it: Imagine that you used ViewModel to persist some utility class through orientation change. Let’s call it Router, for example. The key here is that it has a constructor marked with an @Inject annotation and extends ViewModel.

Later you can add this Router into your VM’s constructor without noticing that the Router extends ViewModel.

What will happen then? The Router will be created alongside your VM and injected into its constructor, but when onCleared() is called for VM, the same method won’t be called for the Router. It might potentially lead to memory leaks and bugs that are quite hard to catch.

Note that the same might happen with injecting more obvious view models into your VM. It would be incorrect to use VM this way, but the best approach doesn’t leave space for error. Otherwise, we’d use Koin or another service locator instead of Dagger.

So how should we avoid this potential problem? We could use @AssistedInject.

5. Use @AssistedInject

If we agree to use @AssistedInject with all classes extending ViewModel, we wouldn’t have the problem from the example above, and we’d also be able to inject additional parameters into our VM.

Let’s alter our ViewModel to work with assisted injection:

Prepare a universal Factory similar to the previous example:

Add a single method for easily creating a VM delegate:

Change AppComponent:

And we can finally create our view model:

This solution is a bit more complicated than the previous one, but it allows us to avoid a rare and complicated problem that would be quite hard to debug, and also provides us with a way to pass some extra data to ViewModel’s constructor, including SavedStateHandle, screen params, and more.

6. A bonus

In my previous article, I proposed an approach that liberates our view models from extending the ViewModel class and also introduces an alternative way to handle cleaning resources in view models. A similar approach is used in this library, so I’m not the only one who’s using it.

If you’d read that article then you may notice that lazyViewModel looks similar to method getOrCreatePersisted from that article, though the latest doesn’t return a delegate.

We can pack all common dependencies from that article into a single subcomponent like that:

Create an extension function for creating this component in fragment:

Add dependency and remove ViewModel superclass from our VM:

Pack lazy and getOrCreatePersisted in one method for simplicity:

And now we can create view models with ease:

This way we don’t have to use assisted injection if we don’t need to pass extra parameters to the view model, and all dependencies of our view model that require cleaning resources will be able to do so by accepting PersistentLifecycle as a constructor parameter. Also, our view model layer won’t depend on the Android framework (though it still depends on Dagger, so no kotlin multiplatform yet).

Conclusion

Although it’s not always easy to find, there are ways to inject parameters into ViewModel with Dagger without losing its compile-time dependency graph verification and a boilerplate code. Especially if you don’t mind getting rid of ViewModel superclass, always creating VM with assisted injection, or keeping an eye for potential nested VM injections.

--

--