Dagger Recipes: Illustrative step by step guide to achieve constructor injection in WorkManager
Recently, while working on an app, I decided to try out Jetpack’s WorkManager for all background jobs (it’s pretty great ). The app already uses Dagger 2 for DI and I wanted to carry that over to WorkManager as well when executing jobs.
You might already know, Android system components are not constructor injectable prior to Pie and currently available solutions use Members injection
and there are variatons in it.
- The first is accessing your injector component from
Application
class and then callingcomponent.inject(this)
which would do a member injection. This approach breaks DI best pratices - the class getting injected should not know its injector and it makes it difficult to access thisActivity
in a modular project. - The second is using
dagger.android
andMultibinding
s to implement a factory + service locator apporach to get the dependencies runtime. Injectable android components only needed to doAndroidInjection.inject(this)
. I initially avoided this approach (lack of good documentation and unknowns), but have learnt to live with it especially in a modular project and this is the goto approach I take.
Why constructor injection?
One pratice I follow after setting up Dagger is everything apart from Android components should be constructor injected. This means once the class is instantiated the dependencies are already resolved, there is no waiting for a lifecycle event to do member injection and you can reduce stateful code by making the dependency immutable i.e val
. This approach has few benefits
- Reduced stateful code in resolving dependencies.
- Improves readability - exactly know what dependencies the class wants.
- No dependency on the injector itself - constuctor injected class can exist in different components. In a isolated module for example, a
TestComponent
can be used to provide dependecies for testing and during usage, the dependencies can provided by the app component.
About this article
With above in mind, in this article, I will walk you through my thought process of how I acheived construction injection in WorkManager
. I believe knowing to walk through dagger errors and then configuring it to successfull compile using various options dagger provides is a helpful skill to have. To do this, I will start with a failing build and then step through ways to help dagger help us.
What we will build
We will have a simple app with a single HelloWorldWorker
. The app will use Repository
pattern in the data layer and Dagger is setup to inject repositories. The HelloWorldWorker
has a dependency on WordsRepository
. We will concentrate on injecting HelloWorldWorker
.
The initial failing setup can be found in this commit.
Concerned Worker
subclass what we would like to inject:
class HelloWorldWorker
@Inject
constructor(
private val application: Application,
workerParameters: WorkerParameters,
private val wordsRepository: WordsRepository // App dependency
) : RxWorker(application, workerParameters) {
override fun createWork(): Single<Result> {
return wordsRepository.sayHelloWorld()
.doOnSuccess { message ->
// Toasts are bad, don't use it.
Toast.makeText(application, message, Toast.LENGTH_SHORT).show()
}.map { Result.success() }
.onErrorReturnItem(Result.failure())
}
}
In the remainder of the article, we will see how this class can be initialized and make it compatible with WorkManager
.
Missing pieces
HelloWorldWorker
’s constructor is marked with @Inject
, we are letting Dagger know that this Worker
participates in DI. Since this class is supposed to be initialized by WorkManager
, we don’t actually request an instance from Dagger but instead WorkManager
does. When we run the app and try to start the Worker
we get:
Could not instantiate in.arunkumarsampath.dagger.workmanager.jobs.HelloWorldWorker
java.lang.NoSuchMethodException: <init> [class android.content.Context, class androidx.work.WorkerParameters]
at java.lang.Class.getConstructor0(Class.java:2320)
at java.lang.Class.getDeclaredConstructor(Class.java:2166)
at androidx.work.WorkerFactory.createWorkerWithDefaultFallback(WorkerFactory.java:91)
at androidx.work.impl.WorkerWrapper.runWorker(WorkerWrapper.java:191)
at androidx.work.impl.WorkerWrapper.run(WorkerWrapper.java:125)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
WorkManager here tries to instantiate HelloWorldWorker
with two params context
and workerParameters
, but we do have a third one wordsRepository
. Before we try to fix the params issue, let’s see if the graph is complete. Dagger, while processing recursively tries to satisfy all dependencies (the ones you provide - more on this later). To force Dagger to check if HelloWorldWorker
’s dependencies can be resolved, let’s request it directly in HomeActivity
.
class HomeActivity : DaggerAppCompatActivity() {
@Inject
lateinit var helloWorldWorker: HelloWorldWorker
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupUi()
}
}
Now when we try to build the app, we get a compile time error:
[Dagger/MissingBinding] androidx.work.WorkerParameters cannot be provided without an @Inject constructor or an @Provides-annotated method.
public abstract interface AppComponent extends dagger.android.AndroidInjector<in.arunkumarsampath.dagger.workmanager.WorkManagerApp> {
^
androidx.work.WorkerParameters is injected at
in.arunkumarsampath.dagger.workmanager.jobs.HelloWorldWorker(…, workerParameters, …)
in.arunkumarsampath.dagger.workmanager.jobs.HelloWorldWorker is injected at
in.arunkumarsampath.dagger.workmanager.home.HomeActivity.helloWorldWorker
... // left for brevity
Looks like Dagger cannot find WorkerParameters
instance but it did not complain about application
or wordsRepository
. The remaining missing piece is how to expose WorkerParameters
at compile time and then letting WorkerManager
use it? Let’s visualize this below.
WorkerFactory
Since one of the recent releases, Google added something called WorkerFactory
whose implementation is used to instantiate Worker
s. It is possible to provide a custom isntance via WorkManager.initialize
. WorkerFactory
has createWorker
method which has all the information we need.
public abstract @Nullable ListenableWorker createWorker(
@NonNull Context appContext,
@NonNull String workerClassName,
@NonNull WorkerParameters workerParameters);
Before we discuss about exposing workerParameters
as a binding, let’s get the setting custom WorkerFactory
instance out of the way. WorkManager uses a ContentProvider
by default to intialize itself (that’s how it gets application
context). We can disable this by using tools:node="remove"
in AndroidManifest.
Setting custom WorkerFactory instances
- Remove content provider initializer by adding the below in
AndroidManifest.xml
<provider android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove" />
- Initialize WorkManager with custom
WorkerFactory
inApplication.onCreate
.
WorkManager.initialize(
application,
Configuration.Builder().run {
setWorkerFactory(workerFactory) // Pass custom worker factory instance
build()
}
)
Now writing a custom WorkerFactory
and exposing workerParameter
s binding from there is pending, visualizing below.
Exposing WorkerParameters
Dagger bindings refresher
First, I’d like to do a refresher on various options we have to expose
bindings to Dagger. For classes we don’t own the constructor, we have to guide Dagger ourselves and that is where @Module
comes in. We typically write methods in @Module
annotated classes and return the Type
we want exposed. This way when this Type
is requested, Dagger uses the method we wrote. In order to not repeat information, I suggest reading this excellent article by Gabor Varadi.
Enter Subcomponents
Above we notice that dagger is able to find Application
instance when we injected HelloWorldWorker
. If we look at the source, while building our root component (AppComponent
), it is possible to pass in parameters using @BindsInstance
. application
is the parameter in this case. One crucial detail is that, types passed in with @BindsInstance
are exposed as binding to the entire graph.
Since Application
is known only at runtime, we use @BindsInstance
to pass the created instance which then becomes available to the whole graph. During compile time validation, the checks pass since @BindsInstance
is considered as binding.
Example:
We already request application
instance in DefaultWordsRepository. This request is satisfied by @BindInstance
in the AppComponent
. It looks like for runtime known types @BindsInstance
is a good candidate for exposing that type. The workerParameters
is known only when createWorker
is called, if we create a component there with @BindsInstance
of type WorkerParameters
we should be able to expose it to Dagger.
Subcomponents
are great candidate for this. It automatically inherits all the bindings from parent component and makes it available within the subcomponent. Let’s update the visualization with this info about how application
and wordsRepository
are satisfied.
Now in order to provide WorkerParameters
, we can create a SubComponent
at createWorker
. Now Dagger should be able to resolve workerParameter
s for HelloWorldWorker
at the constructor as long as HelloWorldWorker
and the exposed WorkerParameters
are in the same component. We will come back to this later, now we will visualize updated approach.
Writing WorkerSubcomponent
As discussed, our WorkerSubcomponent
takes a WorkerParameters
param and it is pretty straight forward to write.
@Subcomponent
interface WorkerSubcomponent {
@Subcomponent.Builder
interface Builder {
@BindsInstance
fun workerParameters(param: WorkerParameters) : Builder
fun build(): WorkerSubcomponent
}
}
Connecting AppComponent and WorkerSubcomponent
In order to let AppComponent
know the WorkerSubComponent
, it is sufficient to write an abstract method that returns WorkSubComponent
’s Builder
in AppComponent
. This is important since without this WorkerSubComponent
won’t inherit Application
or WordsRepository
etc.
Udpated AppComponent:
@Singleton
@Component(
modules = [
AndroidSupportInjectionModule::class,
HomeBuilder::class,
DataModule::class
]
)
interface AppComponent : AndroidInjector<WorkManagerApp> {
// Establish WorkerSubcomponent as subcomponent
fun workerSubcomponentBuilder(): WorkerSubcomponent.Builder
@Component.Builder
interface Builder {
fun build(): AppComponent
@BindsInstance
fun application(application: Application): Builder
}
}
Let us visualize the updated binding graph now.
Connecting the dots
Now since we have a approach to provide a full graph, let’s look into how to use it and actually automate instantiating instances and let WorkManager
use them.
DaggerWokerFactory - custom worker factory
We will start with writing our DaggerWorkerFactory which has following responsibilities
- With
workerParameters
create a subcomponent and complete binding graph forWorker
instances. - With
workerClassName
instantiate theWorker
instance and return it forWorkerManager
to use.
Creating the WorkerSubcomponent
Since we already declared AppComponent.workerSubcomponentBuilder()
in AppComponent
, we can directly request an instance in constructor and use it to create WorkerSubComponent
. Note that the Dagger maintains the internal implementation of the builders. Covering that is beyond the scope of the article.
@Singleton
class DaggerWorkerFactory
@Inject
constructor(private val workerSubcomponent: WorkerSubcomponent.Builder) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
workerSubcomponent
.workerParameters(workerParameters)
.build()
// How to create workerClassName instance?
}
}
After creating the subcomponent, creating the instance with workerClassName
still remains unknown which is covered in the next section.
Creating instances with class name
Dagger already has a mechanism to request dependencies by their type. Within Dagger, it is done using Provider<Type>
and by calling provider.get()
we can get an instance of Type
. To create Worker instance with class name, all we need is Provider<HelloWorldWorker>
.
Note: We can’t get a Provider<T>
without a complete graph for T
. Which means for a incomplete graph, compilaton fails and Provider<T>
is not generated at all.
In order to write it in a generic way i.e using this DaggerWorkerFactory
for instantiating all available Workers
in the app, we will have to do it without hardcoding HelloWorldWorker
type.
Multibindings
From Google docs
Dagger allows you to bind several objects into a collection even when the objects are bound in different modules using multibindings. Dagger assembles the collection so that application code can inject it without depending directly on the individual bindings.
This seems like a perfect fit for our use case which is getting the provider by key from a map. Suppose if we have s Map<Class<out RxWorker>, Provider<RxWorker>>
instance, we can use the workerClassName
from createWorker()
and get the Provider<HelloWorldWorker>
and return the instance.
In order to get Map<Class<out RxWorker>, Provider<RxWorker>>
, we have to configure multibinding. We can do that by using @Binds
and @IntoMap
annotations. We also need to specify the key type (Class<out RxWorker>
) for the Map
which can be done using a custom annotation as shown below.
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention()
@MapKey
annotation class WorkManagerKey(val value: KClass<out RxWorker>)
For dagger to generate this map instance, we have to declare the multibinding generating code in any @Module
. My personal preference to add it as a Builder
class inside the class that participates in map generation, in this case HelloWorldWorker
.
Updated HelloWorldWorker
:
class HelloWorldWorker
@Inject
constructor(
private val application: Application,
workerParameters: WorkerParameters,
private val wordsRepository: WordsRepository
) : RxWorker(application, workerParameters) {
override fun createWork(): Single<Result> {
return wordsRepository.sayHelloWorld()
.doOnSuccess { message ->
// Toasts are bad, don't use it.
Toast.makeText(application, message, Toast.LENGTH_SHORT).show()
}.map { Result.success() }
.onErrorReturnItem(Result.failure())
}
@Module
abstract class Builder {
@Binds
@IntoMap
@WorkerKey(HelloWorldWorker::class)
abstract fun bindHelloWorldWorker(worker: HelloWorldWorker): RxWorker
}
}
If you remember, for binding graph to be complete, the WorkerParameters
exposed and generated map bindings should be in the same component. Since WorkerSubComponent
is responsible for exposing WorkerParameters
we will install the multibinding module to WorkerSubComponent
as follows.
@Subcomponent(modules = [HelloWorldWorker.Builder::class])
interface WorkerSubcomponent {
fun workers(): Map<Class<out RxWorker>, Provider<RxWorker>>
@Subcomponent.Builder
interface Builder {
@BindsInstance
fun workerParameters(param: WorkerParameters): Builder
fun build(): WorkerSubcomponent
}
}
As noted above, for convenience we expose a method to get the binding for Map<Class<out RxWorker>, Provider<RxWorker>>
with fun workers()
. Connecting everything the DaggerWorkerFactory
looks like below.
@Singleton
class DaggerWorkerFactory
@Inject
constructor(private val workerSubcomponent: WorkerSubcomponent.Builder) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
) = workerSubcomponent
.workerParameters(workerParameters)
.build().run {
createWorker(workerClassName, workers())
}
private fun createWorker(
workerClassName: String,
workers: Map<Class<out RxWorker>, Provider<RxWorker>>
): ListenableWorker? = try {
val workerClass = Class.forName(workerClassName).asSubclass(RxWorker::class.java)
var provider = workers[workerClass]
if (provider == null) {
for ((key, value) in workers) {
if (workerClass.isAssignableFrom(key)) {
provider = value
break
}
}
}
if (provider == null) {
throw IllegalArgumentException("Missing binding for $workerClassName")
}
provider.get()
} catch (e: Exception) {
throw RuntimeException(e)
}
}
With the given workerClassName
we find a appropriate Provider<T>
and get an instance and return it to WorkManager
.
Results
The changes for above sections are done in this commit. Debugging the HelloWorldWorker
, we can see that the injection has happened.
The entire source of the sample app with working injection can be found here.
Summary
In this article, we discussed a way to configure dagger for advanced cases where a dependency instance can be accessed only during runtime. In WorkManager
, we get to know about WorkerParameter
only at the time of initialization by WorkerFactory
. In order to handle this case, we break down our root component to sub components and pass in the required type (workParameters) as parameter to a subcomponent. Then we use Multibindings
to instantiate the instances. The implemented solution is a generic one and as long as all Worker
s have a multibinding generator, the injection will work for all available Workers
. Since the initialization happens in onCreate
of the application, even when the WorkManager
wakes up the application to do jobs, the custom factory we provided would be set.
Misc
Thanks for following through till the end of the article. Liked what you read? I would be glad to receive feedback or criticism if any. If you do have any review comments/feedback please reach out to me or express in the comments below. I plan to write more about practical uses of Dagger in this Recipes series.
Comments