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 模式后再统一执行。这样做是为了省电。
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注解 Context和WorkerParameters用@Assisted注解- 其他依赖(如
UploadRepository)正常注入
然后在 Application 类上加上 @HiltAndroidApp,WorkManager 会自动使用 Hilt 的 WorkerFactory 来创建 Worker 实例。你不需要手动配置任何东西。
我刚开始集成的时候犯过一个错:忘了在 Worker 构造函数里加 @Assisted 注解,结果 Hilt 报错说找不到合适的构造函数。后来翻文档才发现,Context 和 WorkerParameters 这两个参数必须由 WorkManager 在运行时提供,所以要用 @Assisted 标记。
知识体系总览
下面这张图是我自己整理的 WorkManager 知识结构,你可以对照着回顾一下:
总结一下
WorkManager 的核心价值就四个字:可靠调度。它不追求最快执行,也不追求最准时执行,而是追求「在合适的时机,用最省电的方式,把任务执行完」。这一点在 Android 碎片化严重的生态下,尤其珍贵。
我个人建议:只要是后台任务,优先考虑 WorkManager。除非你的场景非常特殊(比如闹钟应用需要精确到秒),否则 WorkManager 都能胜任。而且它和 Hilt 的集成非常丝滑,几乎零配置就能用上依赖注入。
好了,这一章的内容就到这里。代码不多,但背后的设计思想值得多琢磨。下次遇到后台任务,不妨先想想:这个任务能不能延迟执行?能不能容忍不精确?如果能,WorkManager 就是你的最佳选择。