Dagger2: Unable to inject dependencies in WorkManager
2020/06 Update
Things become much easier with Hilt and Hilt for Jetpack.
With Hilt, all you have to do is
- add annotation
@HiltAndroidApp
to your Application class - inject out-of-box
HiltWorkerFactory
in the field fo Application class - Implement interface
Configuration.Provider
and return the injected work factory in Step 2.
Now, change the annotation on the constructor of Worker from @Inject
to @WorkerInject
class ExampleWorker @WorkerInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
someDependency: SomeDependency // your own dependency
) : Worker(appContext, workerParams) { ... }
That's it!
(also, don't forget to disable default work manager initialization)
===========
Old solution
As of version 1.0.0-beta01, here is an implementation of Dagger injection with WorkerFactory.
The concept is from this article: https://medium.com/@nlg.tuan.kiet/bb9f474bde37 and I just post my own implementation of it step by step(in Kotlin).
===========
What's this implementation trying to achieve is:
Every time you want to add a dependency to a worker, you put the dependency in the related worker class
===========
1. Add an interface for all worker's factory
IWorkerFactory.kt
interface IWorkerFactory<T : ListenableWorker> {
fun create(params: WorkerParameters): T
}
2. Add a simple Worker class with a Factory which implements IWorkerFactory and also with the dependency for this worker
HelloWorker.kt
class HelloWorker(
context: Context,
params: WorkerParameters,
private val apiService: ApiService // our dependency
): Worker(context, params) {
override fun doWork(): Result {
Log.d("HelloWorker", "doWork - fetchSomething")
return apiService.fetchSomething() // using Retrofit + RxJava
.map { Result.success() }
.onErrorReturnItem(Result.failure())
.blockingGet()
}
class Factory @Inject constructor(
private val context: Provider<Context>, // provide from AppModule
private val apiService: Provider<ApiService> // provide from NetworkModule
) : IWorkerFactory<HelloWorker> {
override fun create(params: WorkerParameters): HelloWorker {
return HelloWorker(context.get(), params, apiService.get())
}
}
}
3. Add a WorkerKey for Dagger's multi-binding
WorkerKey.kt
@MapKey
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class WorkerKey(val value: KClass<out ListenableWorker>)
4. Add a Dagger module for multi-binding worker (actually multi-binds the factory)
WorkerModule.kt
@Module
interface WorkerModule {
@Binds
@IntoMap
@WorkerKey(HelloWorker::class)
fun bindHelloWorker(factory: HelloWorker.Factory): IWorkerFactory<out ListenableWorker>
// every time you add a worker, add a binding here
}
5. Put the WorkerModule into AppComponent. Here I use dagger-android to construct the component class
AppComponent.kt
@Singleton
@Component(modules = [
AndroidSupportInjectionModule::class,
NetworkModule::class, // provides ApiService
AppModule::class, // provides context of application
WorkerModule::class // <- add WorkerModule here
])
interface AppComponent: AndroidInjector<App> {
@Component.Builder
abstract class Builder: AndroidInjector.Builder<App>()
}
6. Add a custom WorkerFactory to leverage the ability of creating worker since the release version of 1.0.0-alpha09
DaggerAwareWorkerFactory.kt
class DaggerAwareWorkerFactory @Inject constructor(
private val workerFactoryMap: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<IWorkerFactory<out ListenableWorker>>>
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
val entry = workerFactoryMap.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) }
val factory = entry?.value
?: throw IllegalArgumentException("could not find worker: $workerClassName")
return factory.get().create(workerParameters)
}
}
7. In Application class, replace WorkerFactory with our custom one:
App.kt
class App: DaggerApplication() {
override fun onCreate() {
super.onCreate()
configureWorkManager()
}
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.builder().create(this)
}
@Inject lateinit var daggerAwareWorkerFactory: DaggerAwareWorkerFactory
private fun configureWorkManager() {
val config = Configuration.Builder()
.setWorkerFactory(daggerAwareWorkerFactory)
.build()
WorkManager.initialize(this, config)
}
}
8. Don't forget to disable default work manager initialization
AndroidManifest.xml
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:enabled="false"
android:exported="false"
tools:replace="android:authorities" />
That's it.
Every time you want to add a dependency to a worker, you put the dependency in the related worker class (like HelloWorker here).
Every time you want to add a worker, implement the factory in the worker class and add the worker's factory to WorkerModule for multi-binding.
For more detail, like using AssistedInject to reduce boilerplate codes, please refer to the article I mentioned at beginning.
I use Dagger2 Multibindings to solve this problem.
The similar approach is used to inject ViewModel
objects (it's described well here). Important difference from view model case is the presence of Context
and WorkerParameters
arguments in Worker
constructor. To provide these arguments to worker constructor intermediate dagger component should be used.
Annotate your
Worker
's constructor with@Inject
and provide your desired dependency as constructor argument.class HardWorker @Inject constructor(context: Context, workerParams: WorkerParameters, private val someDependency: SomeDependency) : Worker(context, workerParams) { override fun doWork(): Result { // do some work with use of someDependency return Result.SUCCESS } }
Create custom annotation that specifies the key for worker multibound map entry.
@MustBeDocumented @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) @Retention(AnnotationRetention.RUNTIME) @MapKey annotation class WorkerKey(val value: KClass<out Worker>)
Define worker binding.
@Module interface HardWorkerModule { @Binds @IntoMap @WorkerKey(HardWorker::class) fun bindHardWorker(worker: HardWorker): Worker }
Define intermediate component along with its builder. The component must have the method to get workers map from dependency graph and contain worker binding module among its modules. Also the component must be declared as a subcomponent of its parent component and parent component must have the method to get the child component's builder.
typealias WorkerMap = MutableMap<Class<out Worker>, Provider<Worker>> @Subcomponent(modules = [HardWorkerModule::class]) interface WorkerFactoryComponent { fun workers(): WorkerMap @Subcomponent.Builder interface Builder { @BindsInstance fun setParameters(params: WorkerParameters): Builder @BindsInstance fun setContext(context: Context): Builder fun build(): WorkerFactoryComponent } } // parent component @ParentComponentScope @Component(modules = [ //, ... ]) interface ParentComponent { // ... fun workerFactoryComponent(): WorkerFactoryComponent.Builder }
Implement
WorkerFactory
. It will create the intermediate component, get workers map, find the corresponding worker provider and construct the requested worker.class DIWorkerFactory(private val parentComponent: ParentComponent) : WorkerFactory() { private fun createWorker(workerClassName: String, workers: WorkerMap): ListenableWorker? = try { val workerClass = Class.forName(workerClassName).asSubclass(Worker::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("no provider found") provider.get() } catch (th: Throwable) { // log null } override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters) = parentComponent .workerFactoryComponent() .setContext(appContext) .setParameters(workerParameters) .build() .workers() .let { createWorker(workerClassName, it) } }
Initialize a
WorkManager
manually with custom worker factory (it must be done only once per process). Don't forget to disable auto initialization in manifest.
manifest:
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove" />
Application onCreate
:
val configuration = Configuration.Builder()
.setWorkerFactory(DIWorkerFactory(parentComponent))
.build()
WorkManager.initialize(context, configuration)
Use worker
val request = OneTimeWorkRequest.Builder(workerClass).build(HardWorker::class.java) WorkManager.getInstance().enqueue(request)
Watch this talk for more information on WorkManager
features.
Overview
You need to look at WorkerFactory, available from 1.0.0-alpha09
onwards.
Previous workarounds relied on being able to create a Worker
using the default 0-arg constructor, but as of 1.0.0-alpha10
that is no longer an option.
Example
Let's say that you have a Worker
subclass called DataClearingWorker
, and that this class needs a Foo
from your Dagger graph.
class DataClearingWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
lateinit var foo: Foo
override fun doWork(): Result {
foo.doStuff()
return Result.SUCCESS
}
}
Now, you can't just instantiate one of those DataClearingWorker
instances directly. So you need to define a WorkerFactory
subclass that can create one of them for you; and not just create one, but also set your Foo
field too.
class DaggerWorkerFactory(private val foo: Foo) : WorkerFactory() {
override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? {
val workerKlass = Class.forName(workerClassName).asSubclass(Worker::class.java)
val constructor = workerKlass.getDeclaredConstructor(Context::class.java, WorkerParameters::class.java)
val instance = constructor.newInstance(appContext, workerParameters)
when (instance) {
is DataClearingWorker -> {
instance.foo = foo
}
// optionally, handle other workers
}
return instance
}
}
Finally, you need to create a DaggerWorkerFactory
which has access to the Foo
. You can do this in the normal Dagger way.
@Provides
@Singleton
fun workerFactory(foo: Foo): WorkerFactory {
return DaggerWorkerFactory(foo)
}
Disabling Default WorkManager Initialization
You'll also need to disable the default WorkManager
initialization (which happens automatically) and initialize it manually.
How you do this depends on the version of androidx.work
that you're using:
2.6.0 and onwards:
In AndroidManifest.xml
, add:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="YOUR_APP_PACKAGE.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
Pre 2.6.0:
In AndroidManifest.xml
, add:
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="YOUR_APP_PACKAGE.workmanager-init"
android:enabled="false"
android:exported="false"
tools:replace="android:authorities" />
Be sure to replace YOUR_APP_PACKAGE with your actual app's package. The <provider
block above goes inside your <application
tag.. so it's a sibling of your Activities
, Services
etc...
In your Application
subclass, (or somewhere else if you prefer), you can manually initialize WorkManager
.
@Inject
lateinit var workerFactory: WorkerFactory
private fun configureWorkManager() {
val config = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
WorkManager.initialize(this, config)
}