9、WorkManager:WorkManager基础、链式任务与周期性任务、WorkManager与Hilt集成

各位同学,今天我们来聊聊 WorkManager。说实话,这个组件是我在 Jetpack 全家桶里最喜欢的一个。为什么?因为它解决了一个非常实际的问题——后台任务。

你想想看,在 Android 开发中,后台任务一直是个老大难。早期的 Service 方案,后来出了 JobScheduler,再后来又有 Firebase JobDispatcher……每次系统版本升级,后台限制就收紧一层。我有个项目在 Android 8 上跑得好好的,到了 Android 10 直接罢工,就是因为没及时迁移到 WorkManager。

WorkManager 的好处在于:它帮你把不同系统版本的后台策略差异给抹平了。你写一套代码,它在 API 14 上用 AlarmManager + BroadcastReceiver,在 API 21 以上用 JobScheduler,在 API 23 以上还能利用 Doze 模式的窗口期。说白了,你只管定义任务,调度的事交给它。

WorkManager 基础:三步走

用 WorkManager 其实就三个步骤:定义任务、配置约束、提交执行。我习惯把它类比成「发快递」——你写好包裹(Worker),填好运单(WorkRequest),然后交给快递站(WorkManager)去配送。

先看一个最简单的例子:

// 第一步:定义 Worker
class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        // 这里执行你的后台任务
        return try {
            uploadFile()
            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

// 第二步:创建 WorkRequest
val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
    )
    .build()

// 第三步:提交给 WorkManager
WorkManager.getInstance(context).enqueue(uploadRequest)

嗯,这里要注意 doWork() 的返回值。我见过不少新手直接返回 Result.success(),哪怕任务失败了也这么干。这样 WorkManager 会认为任务已完成,不会重试。正确的做法是:

  • Result.success():任务成功,不需要重试
  • Result.failure():任务失败,且不需要重试
  • Result.retry():任务失败,需要重试(配合重试策略)
小技巧:如果你想让任务在失败后延迟重试,可以设置后退策略:.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)。指数退避策略在服务器压力大的场景下特别好用,我曾经用它解决过一个图片上传的并发问题。

链式任务:串行与并行

实际项目中,很少有单个任务就能搞定的场景。比如用户上传头像,你可能需要:先压缩图片 → 再上传到 CDN → 最后更新数据库。这三个步骤有先后依赖关系,这就是链式任务的用武之地。

链式任务的核心 API 是 WorkManager.beginWith().then()

val compressWork = OneTimeWorkRequestBuilder<CompressWorker>().build()
val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>().build()
val updateDbWork = OneTimeWorkRequestBuilder<UpdateDbWorker>().build()

WorkManager.getInstance(context)
    .beginWith(compressWork)          // 第一步:压缩
    .then(uploadWork)                 // 第二步:上传
    .then(updateDbWork)               // 第三步:更新数据库
    .enqueue()

如果你需要并行执行某些任务,可以用 beginWith(listOf())

val compressWork = OneTimeWorkRequestBuilder<CompressWorker>().build()
val watermarkWork = OneTimeWorkRequestBuilder<WatermarkWorker>().build()

WorkManager.getInstance(context)
    .beginWith(listOf(compressWork, watermarkWork))  // 压缩和加水印并行
    .then(uploadWork)                                 // 都完成后才上传
    .enqueue()

我在项目中遇到过这样一个坑:链式任务中如果某个 Worker 返回 Result.failure(),整个链会中断,后续的 .then() 不会执行。所以如果你的业务逻辑允许部分失败,记得在 Worker 内部做好异常处理,返回 Result.success() 并传递错误状态。

周期性任务:定时与间隔

有些任务需要定期执行,比如每天同步一次数据、每 15 分钟检查一次新消息。这时候用 PeriodicWorkRequest

val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
    15, TimeUnit.MINUTES
).build()

WorkManager.getInstance(context).enqueue(syncRequest)

这里有个关键点:最小间隔是 15 分钟。你设置 5 分钟也没用,系统会强制拉到 15 分钟。为什么?因为 Android 从 8.0 开始对后台执行做了严格限制,WorkManager 也要遵守这个规则。

另外,周期性任务不是「每 15 分钟准时执行一次」,而是「每 15 分钟有一个执行窗口」。什么意思呢?比如你的手机在 Doze 模式下,WorkManager 会把多个任务的执行窗口合并,等手机退出 Doze 模式后再统一执行。这样做是为了省电。

避坑指南:我曾经在日志里发现周期性任务有时隔了 40 分钟才执行,当时以为是 bug。后来查了文档才明白——WorkManager 不保证准时,只保证在时间窗口内执行。如果你的业务对时间精度要求很高(比如秒级),WorkManager 不适合你,考虑用 AlarmManager。

WorkManager 与 Hilt 集成

到了依赖注入这块,很多同学会问:Worker 的构造函数需要 Context 和 WorkerParameters,Hilt 怎么注入?

答案是:用 @HiltWorker 注解。

先看代码:

@HiltWorker
class UploadWorker @AssistedInject constructor(
    @Assisted context: Context,
    @Assisted params: WorkerParameters,
    private val repository: UploadRepository  // 这是你要注入的依赖
) : Worker(context, params) {

    override fun doWork(): Result {
        return repository.upload()
    }
}

注意几个要点:

  • Worker 类必须用 @HiltWorker 注解
  • 构造函数用 @AssistedInject 注解
  • ContextWorkerParameters@Assisted 注解
  • 其他依赖(如 UploadRepository)正常注入

然后在 Application 类上加上 @HiltAndroidApp,WorkManager 会自动使用 Hilt 的 WorkerFactory 来创建 Worker 实例。你不需要手动配置任何东西。

我刚开始集成的时候犯过一个错:忘了在 Worker 构造函数里加 @Assisted 注解,结果 Hilt 报错说找不到合适的构造函数。后来翻文档才发现,ContextWorkerParameters 这两个参数必须由 WorkManager 在运行时提供,所以要用 @Assisted 标记。

知识体系总览

下面这张图是我自己整理的 WorkManager 知识结构,你可以对照着回顾一下:

WorkManager 基础使用 Worker → doWork() WorkRequest → 约束条件 enqueue() → 提交执行 链式任务 beginWith() → 第一步 .then() → 后续步骤 并行:beginWith(listOf()) 周期性任务 PeriodicWorkRequest 最小间隔 15 分钟 不保证准时执行 Hilt 集成 @HiltWorker + @AssistedInject + @Assisted

总结一下

WorkManager 的核心价值就四个字:可靠调度。它不追求最快执行,也不追求最准时执行,而是追求「在合适的时机,用最省电的方式,把任务执行完」。这一点在 Android 碎片化严重的生态下,尤其珍贵。

我个人建议:只要是后台任务,优先考虑 WorkManager。除非你的场景非常特殊(比如闹钟应用需要精确到秒),否则 WorkManager 都能胜任。而且它和 Hilt 的集成非常丝滑,几乎零配置就能用上依赖注入。

好了,这一章的内容就到这里。代码不多,但背后的设计思想值得多琢磨。下次遇到后台任务,不妨先想想:这个任务能不能延迟执行?能不能容忍不精确?如果能,WorkManager 就是你的最佳选择。


公众号:蓝海资料掘金营,微信deep3321