Dagger2: Unable to inject dependencies in WorkManager
Asked Answered
I

4

25

So from what I read, Dagger doesn't have support for inject in Worker yet. But there are some workarounds as people suggest. I have tried to do it a number of ways following examples online but none of them work for me.

When I don't try to inject anything into the Worker class, the code works fine, only that I can't do what I want because I need access to some DAOs and Services. If I use @Inject on those dependencies, the dependencies are either null or the worker never starts i.e the debugger doesn't even enter the Worker class.

For eg I tried doing this:

@Component(modules = {Module.class})
public interface Component{

    void inject(MyWorker myWorker);
}

@Module
public class Module{

    @Provides
    public MyRepository getMyRepo(){
        return new myRepository();
    }

}

And in my worker

@Inject
MyRepository myRepo;

public MyWorker() {
    DaggerAppComponent.builder().build().inject(this);
}

But then the execution never reaches the worker. If I remove the constructor, the myRepo dependency remains null.

I tried doing many other things but none work. Is there even a way to do this? Thanks!!

Intuition answered 20/9, 2018 at 22:24 Comment(1)
There is an example with Dagger usage from WorkManager team now - medium.com/androiddevelopers/…Pillion
M
41

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)
}
Mechling answered 19/11, 2018 at 14:57 Comment(13)
Does this DaggerWorkerFactory have to depend on all the dependencies of each worker like DaggerWorkerFactory(val depA: DepA, val depB: DepB, val depC: DepC)?Vanhouten
Yes, I believe so. I can't think of a good way to avoid it knowing about all of the dependencies.Mechling
@CraigRussel I could not get this to work. I get a UninitializedPropertyAccessException on the workerFactory field in the Application subclass. Where is it suppose to be initialised? Is this answer still valid for 1.0.0-alpha12?Leonardaleonardi
@sphrak that sounds like Dagger hasn't been set up yet. Are you using the worker factory before you have configured Dagger maybe?Mechling
And yes, this is still valid for 1.0.0-alpha12Mechling
getting workerFactory hasn't been initialized error. @CraigRussellQuarterstaff
@Quarterstaff make sure you have configured Dagger before calling configureWorkManager(). You can see how that looks for real in this codebase: github.com/duckduckgo/Android/blob/develop/app/src/main/java/…. Note how the function to configure dagger (and therefore @inject the WorkerFactory) is done before the function to configure the Worker Factory.Mechling
@CraigRussell Thanks for the reply. I've solved the issue by just injecting my own application class to the App component. :)Quarterstaff
It's rather weak solution to inject dependencies manually in WorkerFactory implementation when it can be done automatically. It would be a real pain if you had for example one hundred different Wokrers.Merrick
@CraigRussell can you share where you came up w/ the Manifest.xml changes? I see that differs from docs: developer.android.com/topic/libraries/architecture/workmanager/…Ballplayer
@tir38, looks like it's still essentially the same XML. You can see a real world example in our DuckDuckGo browser: github.com/duckduckgo/Android/blob/develop/app/src/main/…Mechling
@Merrick There is an example with more "durable" way from Workmanager team - medium.com/androiddevelopers/…Pillion
@VadimKotov unfortunately the approach you've mentioned does not allow to get rid of manually implementing worker factory to delegate to (unlike solution in my answer to this question), but of course you are right using DelegatingWorkerFactory really gives more flexibility.Merrick
V
30

2020/06 Update

Things become much easier with Hilt and Hilt for Jetpack.

With Hilt, all you have to do is

  1. add annotation @HiltAndroidApp to your Application class
  2. inject out-of-box HiltWorkerFactory in the field fo Application class
  3. 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.

Vanhouten answered 1/1, 2019 at 14:51 Comment(5)
Why does Android need to be so freakin' complex?! The ViewModelFactory is so much easier but let's not use that and instead use this utter complex systemCatnip
Thanks a lot for your simplified solution :) I was following this blog but stuck at some point and you were my savior.Girdle
Is there an example project that is functional and works?? Nothing from the above techniques run for me.Labrum
You saved my second day on this topic. Yesterday, I was following the article with AssistedInject, but it always has an error that I can not resolve so I give up on it and try your solution. Thank you so much again. Good job!Escalera
Here's more detail how to use Hilt in WorkerManager proandroiddev.com/hilt-migration-guide-54c48ca18353Canaliculus
M
7

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.

  1. 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
        }
    }
    
  2. 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>)
    
  3. Define worker binding.

    @Module
    interface HardWorkerModule {
    
        @Binds
        @IntoMap
        @WorkerKey(HardWorker::class)
        fun bindHardWorker(worker: HardWorker): Worker
    }
    
  4. 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
    }
    
  5. 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) }
    }
    
  6. 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)
  1. Use worker

    val request = OneTimeWorkRequest.Builder(workerClass).build(HardWorker::class.java)
    WorkManager.getInstance().enqueue(request)
    

Watch this talk for more information on WorkManager features.

Merrick answered 11/11, 2018 at 3:6 Comment(2)
I don't know why but my application compiles and runs but then object from AppModule doesn't get injected into constructor. If I try to inject into field it gets null. Can I use this with typical AppComponent as ParentComponent? how to connect dots?Commercial
@MichałZiobro Yes, you do can use AppComponent as ParentComponent. Don't you forget to annotate your worker's constructor with @Inject annotation? Also you can debug worker providing to investigate the problem, just put breakpoint at provider.get() line of DIWorkerFactory class and then make step into.Merrick
A
4

In WorkManager alpha09 there is a new WorkerFactory that you can use to initialize the Worker the way you want to.

  • Use the new Worker constructor which takes in an ApplicationContext and WorkerParams.
  • Register an implementation of WorkerFactory via Configuration.
  • Create a configuration and register the newly created WorkerFactory.
  • Initialize WorkManager with this configuration (while removing the ContentProvider which initializes WorkManager on your behalf).

You need to do the following:

public DaggerWorkerFactory implements WorkerFactory {
  @Nullable Worker createWorker(
  @NonNull Context appContext,
  @NonNull String workerClassName,
  @NonNull WorkerParameters workerParameters) {

  try {
      Class<? extends Worker> workerKlass = Class.forName(workerClassName).asSubclass(Worker.class);
      Constructor<? extends Worker> constructor = 
      workerKlass.getDeclaredConstructor(Context.class, WorkerParameters.class);

      // This assumes that you are not using the no argument constructor 
      // and using the variant of the constructor that takes in an ApplicationContext
      // and WorkerParameters. Use the new constructor to @Inject dependencies.
      Worker instance = constructor.newInstance(appContext,workerParameters);
      return instance;
    } catch (Throwable exeption) {
      Log.e("DaggerWorkerFactory", "Could not instantiate " + workerClassName, e);
      // exception handling
      return null;
    }
  }
}

// Create a configuration
Configuration configuration = new Configuration.Builder()
  .setWorkerFactory(new DaggerWorkerFactory())
  .build();

// Initialize WorkManager
WorkManager.initialize(context, configuration);
Acevedo answered 21/9, 2018 at 4:27 Comment(2)
I am a little confused. Firstly, what does the variable clazz can't be resolved coz it isn't declared anywhere. I am assuming that I have to use workerKlass over there. Secondly, Where do I need to put the configuration creation part? Also the initialization of workmanager will be done while enqueuing the work request if I am not wrong i.e WorkManager.initialize(context, config).enqueue(something); Sorry too many questions.Intuition
I wish this answer could be more complete. This obviously doesn't work like the injection of a ViewModelFactory. How do you inject your dependencies? What is the "workerClassName" and how do you use it? Do you inject in the factory and declare the factory in Dagger? Or do you inject in the Worker and do something in the factory?Discrepant

© 2022 - 2024 — McMap. All rights reserved.