Dagger2:无法在WorkManager中注入依赖项

25

据我所读,Dagger目前还没有支持在Worker中进行注入的功能。但是有一些人提出了一些解决方法。我尝试过很多种方式,并根据在线示例进行了尝试,但是它们都对我无效。

当我不尝试将任何内容注入到Worker类中时,代码可以正常工作,只是我无法做我想要的事情,因为我需要访问一些DAO和服务。如果我在这些依赖项上使用@Inject,则依赖项要么为null,要么worker从未启动,即调试器甚至不进入Worker类。

例如,我尝试了以下操作:

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

    void inject(MyWorker myWorker);
}

@Module
public class Module{

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

}

在我的工人中

@Inject
MyRepository myRepo;

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

但是执行过程从未到达worker。如果我去掉构造函数,myRepo依赖项仍然为null。

我尝试了很多其他方法,但都不起作用。有没有办法做到这一点?谢谢!!


现在WorkManager团队有一个使用Dagger的示例- https://medium.com/androiddevelopers/customizing-workmanager-with-dagger-1029688c0978 - Vadim Kotov
4个回答

41

概述

您需要查看WorkerFactory,从1.0.0-alpha09开始提供。

以前的解决方法依赖于能够使用默认的0-arg构造函数创建Worker,但是从1.0.0-alpha10开始,这不再是一个选项。

示例

假设您有一个名为DataClearingWorkerWorker子类,并且此类需要来自Dagger图形的Foo

class DataClearingWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {

    lateinit var foo: Foo

    override fun doWork(): Result {
        foo.doStuff()
        return Result.SUCCESS
    }
}

现在,您不能直接实例化这些 DataClearingWorker 实例。因此,您需要定义一个 WorkerFactory 子类,可以为您创建其中之一;不仅创建一个,还要设置您的 Foo 字段。
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
    }
}

最后,您需要创建一个DaggerWorkerFactory,它可以访问Foo。 您可以按照Dagger的正常方式执行此操作。

@Provides
@Singleton
fun workerFactory(foo: Foo): WorkerFactory {
    return DaggerWorkerFactory(foo)
}

禁用默认的WorkManager初始化

您还需要禁用默认的WorkManager初始化(该初始化会自动发生),并手动进行初始化。

如何执行此操作取决于您使用的androidx.work版本:

从2.6.0版本开始:

AndroidManifest.xml中添加:

<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>

在 2.6.0 版本之前:

AndroidManifest.xml 中添加:

 <provider
        android:name="androidx.work.impl.WorkManagerInitializer"
        android:authorities="YOUR_APP_PACKAGE.workmanager-init"
        android:enabled="false"
        android:exported="false"
        tools:replace="android:authorities" />

请确保用你的实际应用程序包替换YOUR_APP_PACKAGE。上面的<provider块位于<application标签内部..它是你的ActivitiesServices等的同级。

在你的Application子类(或其他地方,如果你愿意),你可以手动初始化WorkManager

@Inject
lateinit var workerFactory: WorkerFactory

private fun configureWorkManager() {
    val config = Configuration.Builder()
        .setWorkerFactory(workerFactory)
        .build()

    WorkManager.initialize(this, config)
}

1
这个DaggerWorkerFactory是否必须依赖于每个worker的所有依赖项,例如DaggerWorkerFactory(val depA:DepA,val depB:DepB,val depC:DepC)? - diousk
是的,我相信如此。我想不到一个好的方法来避免它了解所有依赖关系。 - Craig Russell
1
@CraigRussell 我无法让它工作。在应用程序子类中的workerFactory字段上我得到了一个UninitializedPropertyAccessException。它应该在哪里初始化?这个答案对1.0.0-alpha12仍然有效吗? - Niclas
是的,这对于 1.0.0-alpha12 仍然有效。 - Craig Russell
3
WorkerFactory的实现中手动注入依赖项是一种相对较弱的解决方案,因为可以自动完成。如果你有一百个不同的“工作者”,那么手动注入依赖关系将会非常麻烦。 - art
显示剩余8条评论

29

2020/06更新

使用HiltHilt for Jetpack会使事情变得更加容易。

使用Hilt,你只需要:

  1. 在您的Application类上添加注释@HiltAndroidApp
  2. 在应用程序类的字段中注入现成的HiltWorkerFactory
  3. 实现接口Configuration.Provider并在步骤2中返回注入的工作工厂

现在,将Worker构造函数上的注释从@Inject更改为@WorkerInject

class ExampleWorker @WorkerInject constructor(
    @Assisted appContext: Context,
    @Assisted workerParams: WorkerParameters,
    someDependency: SomeDependency // your own dependency
) : Worker(appContext, workerParams) { ... }

就是这样了!

(另外,不要忘记禁用默认的工作管理器初始化)

===========

旧解决方案

从版本1.0.0-beta01开始,这里提供了使用WorkerFactory进行Dagger注入的实现。

这个概念来自于这篇文章https://medium.com/@nlg.tuan.kiet/bb9f474bde37,我只是逐步发布了自己的实现(使用Kotlin)。

===========

这个实现的目的是:

每次您想要向工作者添加依赖项时,都将依赖项放在相关的工作者类中

===========

1. 为所有工作者的工厂添加接口

IWorkerFactory.kt

interface IWorkerFactory<T : ListenableWorker> {
    fun create(params: WorkerParameters): T
}

2. 添加一个简单的工人类和工厂,它实现了IWorkerFactory接口并具有此工作程序的依赖项

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. 为Dagger的多绑定添加一个WorkerKey

WorkerKey.kt

@MapKey
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class WorkerKey(val value: KClass<out ListenableWorker>)

4. 添加一个Dagger模块,用于多绑定worker(实际上是多绑定工厂)。

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.WorkerModule放入AppComponent中。我在这里使用dagger-android来构建组件类。

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. 在1.0.0-alpha09版本发布后,添加自定义WorkerFactory以利用创建worker的能力。

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. 在 Application 类中,用我们自定义的工厂替换 WorkerFactory:

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. 不要忘记禁用默认的工作管理器初始化

AndroidManifest.xml

<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="${applicationId}.workmanager-init"
    android:enabled="false"
    android:exported="false"
    tools:replace="android:authorities" />

就是这样。

每当您想要为工作者添加依赖项时,您将依赖项放在相关的工作者类中(例如此处的HelloWorker)。

每当您想要添加工作者时,请在工作者类中实现工厂,并将工作者的工厂添加到WorkerModule进行多重绑定。

如需更多详细信息,例如使用AssistedInject来减少样板代码,请参阅我在文章开头提到的文章。


5
为什么Android需要这么复杂?!ViewModelFactory要简单得多,但让我们不要使用它,反而使用这个非常复杂的系统。 - Matei Suica
非常感谢你的简化解决方案 :) 我一直在跟随这个博客,但在某个地方卡住了,你是我的救星。 - Navinpd
有没有一个可用且有效的示例项目?我尝试过上面的技术,但都无法运行。 - james04
你救了我的第二天。昨天,我一直在按照AssistedInject的文章进行,但它总是有一个错误,我无法解决,所以我放弃了并尝试使用您的解决方案。再次非常感谢您。干得好! - Hien Quang Le
以下是如何在WorkerManager中使用Hilt的更多详细信息:https://proandroiddev.com/hilt-migration-guide-54c48ca18353 - baderkhane

7
我使用Dagger2 Multibindings来解决这个问题。
类似的方法也用于注入ViewModel对象(在这里有很好的描述)。与视图模型情况不同的是,在Worker构造函数中存在ContextWorkerParameters参数。为了将这些参数提供给工作者构造函数,需要使用中间dagger组件。
  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.

清单:

    <provider
        android:name="androidx.work.impl.WorkManagerInitializer"
        android:authorities="${applicationId}.workmanager-init"
        android:exported="false"
        tools:node="remove" />

应用程序 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)
    

观看此演讲以获取更多关于WorkManager功能的信息。


我不知道为什么,但我的应用程序编译并运行,但是 AppModule 中的对象没有被注入到构造函数中。如果我尝试注入到字段中,它会得到 null。我可以将其与典型的 AppComponent 作为 ParentComponent 使用吗?如何连接这些点? - Michał Ziobro
@MichałZiobro 是的,你可以使用AppComponent作为ParentComponent。不要忘记在你的worker构造函数上注释@Inject注解。此外,您可以调试工作程序以调查问题,只需在DIWorkerFactory类的provider.get()行处设置断点,然后进行步进即可。 - art

4
在 WorkManager alpha09 中,有一个新的 WorkerFactory 可以用来按照你想要的方式初始化 Worker
  • 使用新的 Worker 构造函数,它接受一个 ApplicationContextWorkerParams
  • 通过 Configuration 注册一个 WorkerFactory 的实现。
  • 创建一个 configuration 并注册新创建的 WorkerFactory
  • 使用该配置初始化 WorkManager(同时删除为你初始化 WorkManagerContentProvider)。

你需要执行以下操作:

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);

我有点困惑。首先,变量clazz无法解析,因为它没有在任何地方声明。我假设我需要在那里使用workerKlass。其次,我需要把配置创建部分放在哪里?如果我没记错的话,工作管理器的初始化将在排队工作请求时完成,即WorkManager.initialize(context, config).enqueue(something)。抱歉问题太多了。 - varunkr
2
我希望这个答案能更完整一些。这显然不像ViewModelFactory的注入那样工作。你如何注入你的依赖项?"workerClassName"是什么,你如何使用它?你是在工厂中注入并在Dagger中声明工厂吗?还是在Worker中注入并在工厂中执行某些操作? - Rui

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接