Dagger 2 setup with WorkManager: a complete step by step guide to perform constructor injection on Worker class

Illustration by Virginia Poltrack

Table of content:
 — TL; DR
 — Introduction
 — Goal
 — Problems
 — Solution
 — Step by step implementation
 — Understand the generated code
 — Summary
 — Source code
 — Alternative setups
 — Reference

TL; DR

We cannot perform constructor injection in Worker class because of the params and appContext is only available at runtime, so we perform the constructor injection with its Factory instead. Then set up a multi binds map with those factories, inject this map of Factory inside a custom WorkerFactory then create new Worker instance from there. Fully working sample project can be found here

Introduction

Edit 6 March 2019: WorkManager is now officially stable
Ref: https://dl.google.com/dl/android/maven2/index.html
https://twitter.com/ianhlake/status/1103011347858513923

WorkManager just hit beta a few days ago, in the release note, the team has mentioned:

This release contains no API changes; moving forward, WorkManager is expected to stay API stable until the next version

I guess this is a good time to write about it, more specifically, how to inject dependence into Worker class?

In this tutorial, we‘re not discussing the basis of WorkManager rather than a proper Dagger 2 setup with it. So if you are new to WorkManager, I recommend checking out the official document

Goal

At the end of this tutorial, not only you can perform constructor injection on Worker class but also fully understanding how things work.

Problems

In the above code snippet, the first problem is that Worker are instantiated by WorkerManager (like Activity and Fragment are instantiated by Android framework) not by us. This means you can’t pass any other parameter as the dependencies in the constructor expect the Context and WorkerParameters, therefore, it is almost impossible to perform constructor injection on Worker class. This left out for us the only option is field injection.

Nobody like field injection, in some way or another, it requires the class to know about its injector.

Solution

In the alpha 9 release, the Android team introduce a new abstract class called WorkerFactory

A factory object that creates ListenableWorker instances. The factory is invoked every time a work runs

In a nutshell, if there is a custom factory registered to WorkManager (let call it SampleWorkerFactory), every time a new worker is requested, WorkManager will ask SampleWorkerFactory to construct a new worker instance. This is great because through our custom factory we can now decide how to construct Worker instance, not restricted to the default constructor anymore.

TL; DR with the introduction of WorkerFactory we can now perform constructor inject in our worker.

Step by step implementation

We will come back to SampleWorkerFactory later, know let focusing on the implementation below:

HelloWorldWorker need Foo as a dependency. We cannot do a constructor injection on Foo as mentioned. So, we perform constructor injection in its inner class HelloWorldWorker.Factory instead. The factory class can have any name as long as it implements ChildWorkerFactory.

Why do we need ChildWorkerFactory interface? This interface, later on, becomes useful since our setup invokes Dagger multi-bind (similar setup with ViewModel in google/iosched).

Note: we now have 2 types of factory:
WorkerFactory: a custom factory that we register with WorkManager, our implementation is SampleWorkerFactory
ChildWorkerFactory: a factory for each Worker that supports for the constructor injection purpose (HelloWorldWorker.Factory)

Now, all of the ChildWorkerFactory implementations are available in the dependence graph. Now we bind those ChildWorkerFactory into a multi-bind map.

Then, inject that map into our custom WorkerFactory akaSampleWorkerFactory — the custom factory that we will register with WorkerManager. You did not hear me wrong, factories inside a factory.

Remember to bind SampleWorkerFactory to WorkerFactory as well.

One final important step, register SampleWorkerFactory to WorkManager one to our Application and one in AndroidManifest.xml

We then hit the run button…

D/HelloWorldWorker: Hello world!
D/HelloWorldWorker: Injected foo: com.sample.daggerworkmanagersample.Foo@215b58d0
I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=c1628749-ed19-4b11-b027-95031d3b3bae, tags={ com.sample.daggerworkmanagersample.HelloWorldWorker } ]

It works! but...

The problem is not stopping there. Despise the double factory pattern work really well, the worst part is we have to implement every single ChildWorkerFactory manually, this example only has 1 dependence (Foo) imagine we have 10 dependence for each Worker and our Application have a total of 10 Worker? Technically we can still do that, but you know how it goes, this cannot end up well for us.

​This is where AssistedInject comes to play. A library by Square that compatible with Dagger 2. It has 2 jobs

  • Generates all of the ChildWorkerFactory implementations
  • Bind those implementations to the dependence graph via a module (more detail later)

So it generates 2 thing factories and a module that includes all the implementation bindings

​Setup our existing code base with AssistedInject is simple. Annotate Worker class with @AssistedInject. Any parameters that we want to create with the generated factory, annotate it with @Assisted. And for the factory (original was class that now become interface), annotate it with @AssistedInject.Factory. Our Worker classes now look a lot of cleaners and the most fun part is we now don't have to write these boilerplate factory codes anymore.

​Since AssistedInject binds those factories inside a module. Declare a module that includes the generated module, annotate it with @AssistedModule, add it to our Component. This makes all of the ChildWorkerFactory implementations available in our component dependence graph.

We hit the fun button again and all is working as expected.

Understand the generated code

It’s always the good thing when we can understand what is going under the hood, not just rely on “magic”, so let dive into the generated source code.

First off, the generated implementation of HelloWorldWorker.Factory, look almost the same as our original code. Next, the generated module (a.k.a AssistedInject_SampleAssistedInjectModule) AssistedInject simply binds HelloWorldWorker_AssistedFactory to HelloWorldWorker.Factory.

Summary

We when from:

  • Cannot control how Worker instance is instanced
    => register a custom WorkerFactory to WorkerManager
  • Cannot perform constructor injection on Worker class
    => use factory pattern
  • Do not want to implement those factories manually
    => use AssistedInject

The end result: we can perform a constructor injection in a Worker class, the injection code is generated by AsssistedInject instead of Dagger, the process is automated. The most important part we have followed a core principle of dependency injection:

a class shouldn’t know anything about how it is injected.

Source code

Alternative setups

Previously, there is a lot of discussions when this problem rises up. To summary, there are 2 possible solutions for this. They are all working as expected, you can’t even tell the performance difference. One of them even has a clear advantage over this setup. So it’s worth to mention them, leaving the desition to you.

SubComponent

As mention, appContext and params instance are only available at run time, just like Activity, Fragment and Fragment’s argument. Because of that, we can the same approach with Activity, Fragment etc. Read more about that here. Credit to arunkumar9t2

Advantage: no third library invoke (AssistedInject)

Member injection

Probably the easiest setup, since it invokes only an injection method declaration inside our Component and a base worker class that perform seeking component and inject. No AssistedInject, no dagger-android, no multi-binding, no ChildWorkerFactory, no WorkerFactory. Littery everything, just a method, and a base class and you are good to go.

Advantage: super easy to set up, work best for new developers.

So what is my setup’s advantage? why do we need to spend such an effort on this huge setup? The answer, my friend, is TESTING. Since everything is injected through the constructor, we can easily mock our dependence to test the Worker implementation.

Reference

Special thank to: