Dagger Tips: Leveraging AssistedInjection to inject ViewModels with SavedStateHandle, and Map-Multibinding to constructor-inject Workers in WorkManager using WorkerFactory

Gabor Varadi
ProAndroidDev
Published in
8 min readMay 23, 2020

--

Dependency injection is great. It can massively simplify our code, manage the lifecycle, instance count and scope of our object instances, and the more powerful DI solutions can even automatically resolve the dependency graph for us, potentially just by adding a single annotation to our constructor (@Inject), without us having to manually configure providers for our classes.

Dagger2 is one of these “more powerful DI solutions”: just by adding @Singleton on the class, and @Inject on the constructor, we can instruct Dagger to be able to “inject” the correct scoped instance of our class to any other unscoped or singleton (or subscoped) class that has an @Inject constructor. It’s great!

(For more information on Dagger, I wrote a tutorial a while ago on the basics, which you can find here.)

Complications

Life isn’t always so easy. Sometimes, we can’t just add @Inject to the constructor, and expect things to “magically” work. Some classes are not our own, and therefore cannot be added to Dagger’s graph with this annotation. In these cases, we need a @Provides method in a @Module, which if needs to be scoped, we can add @Singleton to this provider method.

And sometimes, things are even more tricky. You need a runtime argument to construct your class, and therefore cannot directly create it in a @Provides method inside a @Module.

So what can you do to provide a class with runtime arguments?

1.) create a subcomponent per each “place where you need a runtime argument inside the graph”, and pass it in via @Subcomponent.Factory and @BindsInstance.

2.) inject a factory class that exposes a method, with which we can pass in the runtime argument, and ask it to create the instance of the class there and then.

The first option can be useful when there are multiple levels of scope inheritance. In my experience, this has been rather uncommon (and where multiple nested scoped subcomponents were required, they were generally swapped out for component dependencies that expose provision methods anyway), so I will not focus on it. The need for subcomponents generally comes in only when subscopes are required, which in this case is not.

The second option is promising: we can have a (generally, as it is stateless) unscoped factory, injected just as any other class, and we can store and use the created class where we need it.

However, having to manually pass every dependency from Dagger on top of the runtime arguments to the constructor can seem daunting. We’ve lost the simplicity of “add @Inject, use class”. Enter assisted injection.

AssistedInject

Jake Wharton’s AssistedInject library aims to help decrease the boilerplate of manually propagating the Dagger-injected dependencies along with the runtime-provided ones, while avoiding the potential pitfalls introduced by google/auto/factory. (which is not as well-maintained, and while factory classes are auto-generated, it makes you reference generated code in your app — something that AssistedInject tries to minimize by design).

Now, instead of

Without Assisted-Inject

You can do

With Assisted-Inject

As you can see, we no longer have to manually propagate A, B and C to the factory (and keep them as field references), only the runtime argument is needed. Also, the provider method is automatically generated for us (and gets automatically included through the generated module, AssistedInject_AssistedInjectionModule).

How do I make it work?

What you need is the following:

build.gradle for Assisted-Inject

( kapt { correctErrorTypes true } is needed, because we must reference generated class in the Dagger configuration, and otherwise it doesn’t work.)

And with that, we also need to add one more slightly magical thing:

AssistedInjectionModule

This will allow AssistedInject to add the necessary providers to our graph: we are referencing generated code, but only here, this one time.

Now we need to include this AssistedInjectionModule into our component.

SingletonComponent

Now if we define our D.Factory like above:

D.Factory

Then usage is as simple as this:

MyClass that receives D.Factory from Dagger

Just what we wanted!

You can also see it in full action in my Jetpack Navigation FTUE Sample (which contains a complete example for ViewModels that are NavGraph-scoped, and receive a SavedStateHandle in their constructor).

When do I want to use this?

There are two primary use-cases (that I can think of) for using AssistedInject.

The first is to support the injection of ViewModels with a SavedStateHandle, as to create a SavedStateHandle, we must use the AbstractSavedStateViewModelFactory, and to create an AbstractSavedStateViewModelFactory, we need a SavedStateRegistryOwner ( AppCompatActivity/ Fragment / NavBackStackEntry).

As we need a reference to the SavedStateRegistryOwner, we must either use approach 1 (subcomponent) or 2 (injection of factory).

As the created ViewModel is technically super-scope of the injection target (as it lives in the non-config scope, and not the activity/navgraph/fragment scope), the subcomponent approach is only safe if only the ViewModel is injected from the subcomponent, and nothing else.

Otherwise, with the subcomponent approach (see the example), you could easily get the wrong instance of the provided classes (especially if they were otherwise unscoped), or possibly even memory leaks (imagine injecting a class that receives the bound Context parameter, and is injected into the ViewModel transitively! It will never be refreshed, as the ViewModels will not be recreated!)

This objectively translates to “it’s not safe at all” (we’ll see how Dagger-Hilt will workaround this issue when it’s ready), so for now, we should definitely prefer to use the second option: injection of a Factory (that allows the creation of a ViewModel, that we will pass to be used inside an AbstractSavedStateViewModelFactory).

Injecting ViewModel + SavedStateHandle

I actually went into great detail on the following approach in a previous article, explaining why you would prefer to create a Factory per each ViewModel, rather than use Map Multibinding — which means I won’t go into great detail here, just on the essentials.

To get a SavedStateHandle (which allows our state inside the ViewModel to survive across process death by saving it to a Bundle, internally using onSaveInstanceState and then have it restored for us), we need to use an AbstractSavedStateViewModelFactory — and to create an AbstractSavedStateViewModelFactory, we need a SavedStateRegistryOwner.

The SavedStateRegistryOwner is the ComponentActivity, Fragment, or NavBackStackEntry, therefore when we create an AbstractSavedStateViewModelFactory, it must be provided as runtime argument.

We can use a trick: this AbstractSavedStateViewModelFactory can be made universal for all ViewModels, and can therefore actually be inlined as an anonymous instance using some top-level helper functions, and otherwise just pass in a lambda function that takes SavedStateHandle as an argument (which will actually create the ViewModel instance).

With another trick, we can also significantly reduce the actual code needed by ViewModelProvider(viewModelStore, viewModelProviderFactory) using createViewModelLazy .

This allows us to create assisted-injected ViewModels that receive SavedStateHandle in its constructor, without a lot of actual boilerplate at call site, using our new custom by navGraphSavedStateViewModels (or by fragmentSavedStateViewModels) delegates. As long as we can get access to the component (or the ViewModel’s factory), we can pass it to the delegate.

(In case we didn’t need a SavedStateHandle, we would be able to use regular @Inject constructor for our ViewModel, and then expose Provider<MyViewModel> instead of having to create a custom, assisted MySavedStateViewModel.Factory.)

Full sample on how to get SavedStateHandle into ViewModels

Constructor injection of Workers for WorkManager

As I’ve explained in a previous article, you don’t need map-multibinding for ViewModels, because you don’t need to create one instance of the ViewModelProvider.Factory that can create N types of ViewModel.

That is not the case with WorkManager. In the case of WorkManager, you DO need a single WorkerFactory.

What’s even trickier is that we must pass appContext, workerParams to our Worker/ListenableWorker base class, that we get as runtime arguments in WorkerFactory — and we need one Factory instance that can create N worker types, and these workers need runtime arguments. Can you hear it? The sound of map-multi-bound assisted factories?

Connecting the dots

With that in mind, we can actually implement this in about 60 lines of code, though if it were that easy, I would not dedicate an article to it. :)

Map multi-binding is pretty mysterious to set up, but worth it when it’s useful or needed (like in this case).

Full sample on how to constructor-inject Workers for WorkManager using WorkerFactory

So what we did is:

  • create a shared superclass (interface) over the assisted injected Worker factories (one for each worker), so that we can collect them together, and use map-multibinding over it: AssistedWorkerFactory.
  • we define a key that takes the class of the Worker, which can be used for map multi-binding: WorkerKey.
  • we create a module that uses @Binds @IntoMap @WorkerKey to collect each __Worker.Factory as an AssistedWorkerFactory in our Dagger component.
  • once we have a Map that binds together Class<? extends ListenableWorker> with a Provider<AssistedWorkerFactory<? extends ListenableWorker>>, we can index this map using the Class of the worker, that we can retrieve via Class.forName(workerClassName).
  • using these factories, and selecting the right factory, we can finally create Dagger-injected Worker classes that also receive their required runtime arguments.

With that, we actually have a global WorkerFactory that can create every worker instance with both provided dependencies, and the runtime arguments. Rejoice!

In this case, unless we did a massive switch-case (or when) statement over the workerClassName, we wouldn’t be able to simplify the creation of the WorkerFactory . Not even with subcomponents. Meaning, in this case, unless we want to do a when(clazz) and select the right factory manually, we have to use map-multibinding.

Technically, map-multibinding in this case is optional. However, if the workers are defined in separate compilation modules, then map-multibinding is actually unavoidable.

Conclusion

Hope that helped you see how to leverage the power of Dagger’s map-multibinding functionality to create a single instance of a factory that can create N types, and how to utilize Jake Wharton’s AssistedInject library to somewhat simplify the creation of injectable factories for classes that require runtime arguments.

Sometimes, a bit of configuration complexity is necessary to get things done to solve particular problems.

If you need to use some tricks, or magical-looking Dagger configuration, then you should aim to solve a problem, rather than make the life of your (and potential future maintainers of your code) more difficult than it needs to be. Fancyness might seem helpful in the heat of the moment, but if it just raise eyebrows and increases cognitive complexity, it might not be worth it.

Try not to leave a desolate path of ashes behind you, blazing through the codebase with “overly smart solutions” nobody else understands, as the over-eager Enthusiastic Architect, who would create code that makes others curse your name in your future absence. Be smart, write simple code, but do write the code that’s necessary. And use the tools available to you that make your code simpler, more reliable, easier to read and understand, and easier to maintain (and doesn’t have large cost, unlike non-incremental annotation processors).

Just don’t look at the map-multibinding configuration too often, because in Kotlin, it looks like a mess. 😅

In this case though, the complexity is justified, and I hope you found the guide helpful.

Special thanks to Zookey for the request to write this article on configuring the map-multibound assisted factories for the constructor injection of Workers for the WorkerFactory in WorkManager.

Also thanks to Ahmed I. Khalil for his help with the creation of ViewModelUtils.kt.

And if you want, follow (and join!) the Reddit discussion on /r/android_devs.

--

--

Android dev. Zhuinden, or EpicPandaForce @ SO. Extension function fan #Kotlin, dislikes multiple Activities/Fragment backstack.