使用 Dagger 自定义 WorkManager

WorkManager 是一个 Android Jetpack 扩展库,它可以让您轻松规划那些可延后、异步但又需要可靠运行的任务。对于绝大部分后台执行任务来说,使用 WorkManager 是目前 Android 平台上的最佳实践。

如果您一直关注本系列文章,则会发现我们已经讨论过:

在本篇文章中,我们将会讨论使用 Dagger 自定义配置相关的内容,包括:

  • 在我们的 WorkerFactory 中使用 Dagger 注入参数
  • 按需初始化

:warning: 本文扩展自上一篇自定义 WorkManager 的文章。强烈建议在阅读本文之前先去阅读 上一篇文章

为什么是 Dagger

Dagger 是 Android 开发的首选依赖注入库,Google 正积极参与它的开发。如果您还没开始使用 Dagger,或者希望了解更多有关它的信息,请查阅以下资料:官方指南Codelab 实战教程 以及我们近期发布的关于在 最新 Android Studio 中使用 Dagger 导航 的文章。

行文中我假设您对 Dagger 库和依赖注入概念均已有所了解。

即使您正在使用其他的依赖注入库,或者根本没有使用依赖库,本文所呈现的概念依然会对您有所帮助。

回顾

上一篇文章 中,我们探索了如何自定义 WorkManager,其中包括如何使用 DelegatingWorkerFactory将附加的参数传递到 Worker 中。在本篇文章中,让我们看一看如何使用 Dagger 注入这些参数。

使用 Dagger 将参数注入到 WorkerFactory

如果您当前已经在使用 Dagger 来管理依赖,那么首先需要将 Dagger 集成到您的 WorkerFactory 中。如果您使用 Dagger 在您的应用中传递 Retrofit 服务的引用,而且您想要将其传递给您的 Worker,则需要使用 Dagger 将该引用注入到自定义的 WorkerFactory 中。这样一来,WorkFactory 就可以使用 Retrofit 的引用作为额外参数来初始化您的 Worker。

假设这次我们有了 Dagger 注入的 Retrofit 服务的引用。但是这并没有改变 WorkManager 需要自定义工厂和自定义配置的局面。简单来说,我们将用 Dagger 把新的参数注入到我们的工厂中。

/* Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
@Singleton
class MyWorkerFactory @Inject constructor(
    service: DesignerNewsService
) : DelegatingWorkerFactory() {
    init {
        addFactory(myWorkerFactory(service))
 // Add here other factories that you may need in your application
 // 在这里添加您应用中可能会使用的其他工厂
    }
}

:warning: 提示:如果想要 Dagger 能够注入这个值,我们必须把它放进 Dagger 的图中。这就是为什么我们给 Factory 添加了一个 @inject 注解。

本示例中,我们在 Application 里使用一个 AppComponent 来设置 Dagger。AppComponent 稍后会在 MyApplication 中初始化,从而让 Dagger 可以进行成员注入:

/* Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {

  @Component.Factory
  interface Factory {
    fun create(@BindsInstance context: Context): AppComponent
  }

  fun inject(application: MyApplication)
}

随后,我们在 Application 类中注入我们的组件:

/* Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
class MyApplication : Application(), Configuration.Provider {

    // other @Inject variables
    // 另一个 @inject 变量

    lateinit var appComponent: AppComponent

    override fun onCreate() {
        super.onCreate()
        appComponent = DaggerAppComponent.factory().create(applicationContext)
        appComponent.inject(this)
    }
   ...
}

由于 Dagger 已经知道如何提供 MyWorkerFactory 实例,您现在可以通过使用 @Inject 来从 Dagger 图中获取 MyWorkerFactory,并在 getWorkManagerConfiguration 方法中进行使用。

/* Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
class MyApplication : Application(), Configuration.Provider {

    @Inject lateinit var myWorkerFactory: MyWorkerFactory 
    ...

    override fun getWorkManagerConfiguration(): Configuration =
        Configuration.Builder()
                     .setMinimumLoggingLevel(android.util.Log.INFO)
                     .setWorkerFactory(myWorkerFactory)
                     .build()
    ...
}

项目中的其他部分保持不变。

生产环境示例

在使用中型或大型数据库时,Dagger 的表现十分亮眼。我们升级了 Google I/O 与 Android 开发峰会的时间表应用:iosched,使其用上 WorkManager 和 Dagger,它同时也是我们用于展示协程 Flow 最佳实践的应用,详情请查看文章:基于 Android 开发者峰会应用的协程 Flow 最佳实践

2019 Android 开发者峰会应用 中,JobScheduler 被 WorkManager 所取代,用于强制更新时间表。为了能将时间表的紧急更新强制推送至设备,我们为应用添加了这个功能。

在这种情况下,我们需要在 Worker 中使用的额外参数是 refreshEventDataUseCase。您可以在 github 的 iosched 仓库中的 ADSsched 分支 中查看引入了此功能的提交 (commits)。

让我们从 Worker 本身开始,看看最重要的几部分:

ConferenceDataWorker.kt

/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
/**
 * A Job that refreshes the conference data in the repository (if the app is active) and
 * in the cache (if the app is not active).
 * 一个用来刷新资源库(当应用活跃时)以及缓存(当应用不活跃时)中的会议数据的任务。
 */
class ConferenceDataWorker(
    ctx: Context,
    params: WorkerParameters,
    private val refreshEventDataUseCase: RefreshConferenceDataUseCase
) : CoroutineWorker(ctx, params) {

    override suspend fun doWork(): Result {

        Timber.i("ConferenceDataService triggering refresh conference data.")

        return try {
            refreshEventDataUseCase(Unit)
            Timber.d("ConferenceDataService finished successfully.")
            // Finishing indicating this job doesn't need to be rescheduled.
            // Finishing 意味着这个任务不需要被重新安排执行。
            Result.success()
        } catch (e: Exception) {
            Timber.e("ConferenceDataService failed. It will retry.")
            // Indicating worker should be retried
            // 意味着 Worker 应该被重试
            if (runAttemptCount < MAX_NUMBER_OF_RETRY) {
                Result.retry()
            } else {
                Result.failure()
            }
        }
    }
}

源码:ConferenceDataWorker.kt

如您所见,由于在 WorkerFactory 级别处理了参数的传递,因此在 Worker 类上没有 Dagger 注解。

这个参数是 Dagger 已知的,因此可以将其直接注入到我们的自定义 WorkerFactory 中:

/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
class ConferenceDataWorkerFactory(
    private val refreshEventDataUseCase: RefreshConferenceDataUseCase
) : WorkerFactory() {

    override fun createWorker(
        appContext: Context,
        workerClassName: String,
        workerParameters: WorkerParameters
    ): ListenableWorker? {

        return when (workerClassName) {
            ConferenceDataWorker::class.java.name ->
                ConferenceDataWorker(appContext, workerParameters, refreshEventDataUseCase)
            else ->
                // Return null, so that the base class can delegate to the default WorkerFactory.
                // 返回 null,这样基类就可以代理到默认的 WorkerFactory
                null
        }
    }
}

源码:ConferenceDataWorkerFactory.kt

这里仍然没有 Dagger 注解… 原因是我们使用了一个 DelegatingWorkerFactory 来协调那些单个的工厂(此时,我们在 IOsched 中只有一个工厂,但是我们以一种在需要时可以直接添加更多工厂的方式来构建它):

IoschedWorkerFactory.kt

/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
@Singleton
class IoschedWorkerFactory @Inject constructor(
    refreshConferenceDataUseCase: RefreshConferenceDataUseCase
) : DelegatingWorkerFactory() {
    init {
        addFactory(ConferenceDataWorkerFactory(refreshConferenceDataUseCase))
    }
}

源码:IoschedWorkerFactory.kt

OK,这里就是使用 Dagger 向我们的构造函数注入参数的地方。

在这个应用中,我们决定使用按需初始化,并且使用 Dagger 注入所有配置:

/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
@Inject lateinit var workerConfiguration: Configuration

// 使用 DelegatingWorkerFactory 为 WorkManager 设置自定义配置
override fun getWorkManagerConfiguration(): Configuration {
    return workerConfiguration
}

源码: MainApplication.kt

这使我们可以为不同的构建类型注入不同的配置。尤其是,我们为调试构建注入了日志级别设置为 DEBUG 的配置:

/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
@Singleton
@Provides
fun provideWorkManagerConfiguration(
    ioschedWorkerFactory: IoschedWorkerFactory
): Configuration {
    return Configuration.Builder()
        .setMinimumLoggingLevel(android.util.Log.DEBUG)
        .setWorkerFactory(ioschedWorkerFactory)
        .build()
}

源码:debugRelease SharedModule.kt

同时,发布版本使用默认调试级别来设置自定义工厂:

SharedModule.kt (在 shared/src/staging/j/c/g/s/a/i/shared/di/ 中)

/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
@Singleton
@Provides
fun provideWorkManagerConfiguration(
    ioschedWorkerFactory: IoschedWorkerFactory
): Configuration {
    return Configuration.Builder()
        .setWorkerFactory(ioschedWorkerFactory)
        .build()
}

源码:staging SharedModule.kt

当我们首次获取 WorkManager 实例时,WorkManager 将按需初始化。当我们收到 Firebase 消息以获取新的时间表时,就会触发这个操作:

IoschedFirebaseMessagingService.kt

/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
private fun scheduleFetchEventData() {
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build()

    val conferenceDataWorker = OneTimeWorkRequestBuilder<ConferenceDataWorker>()
        .setInitialDelay(MINIMUM_LATENCY, TimeUnit.SECONDS)
        .setConstraints(constraints)
        .build()

    val operation = WorkManager.getInstance(this)
        .enqueueUniqueWork(
            uniqueConferenceDataWorker,
            ExistingWorkPolicy.KEEP,
            conferenceDataWorker)
        .result

    operation.addListener(
        { Timber.i("ConferenceDataWorker enqueued..") },
        { it.run() }
    )
}

源码:IoschedFirebaseMessagingService.kt

至此,我们结束了探索如何使用 Dagger 把参数注入到您的 Worker,同时也了解了如何将 WorkManager 集成到 iosched 这类的大型应用中。

总结

WorkManager 是一个功能十分强大的库,它的默认配置已经可以覆盖许多常见的使用场景。然而当您遇到某些情况时,诸如需要增加日志级别或需要把额外参数传入到您的 Worker 时,则需要一个自定义的配置。

希望通过最近两篇文章所做的介绍,能让您对自定义 WorkManager 有一个良好的认识。如果您有任何疑问,可以在评论区中留言。

编码愉快!