使用 Dagger 自定义 WorkManager

image

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

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

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

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

⚠️ 本文扩展自上一篇自定义 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
 // 在这里添加您应用中可能会使用的其他工厂
    }
}

⚠️ 提示:如果想要 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 有一个良好的认识。如果您有任何疑问,可以在评论区中留言。

编码愉快!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266