任务状态与观察:WorkInfo 与 LiveData/Foreach

说实话,刚开始用 WorkManager 的时候,我踩过一个坑——任务提交了,但不知道它到底跑没跑完。那时候我还在用轮询的方式去查状态,现在想想真是又笨又累。后来我才发现,WorkManager 本身就提供了非常优雅的观察机制。

这一节,我们就来聊聊怎么实时“盯住”你的后台任务。

WorkInfo:任务的“体检报告”

每个 WorkManager 任务在执行过程中,都会生成一个 WorkInfo 对象。你可以把它理解成任务的“体检报告”——里面记录了任务的 ID、当前状态、输出数据、标签等信息。

我个人习惯,在调试阶段一定会打印 WorkInfo 的完整内容。它长这样:

WorkInfo {
    id: 3a2f1b4c-...
    state: SUCCEEDED
    outputData: { "result": "success" }
    tags: [ "sync_work", "user_123" ]
}

你看,状态、输出、标签一目了然。有了它,你就不需要自己维护任务状态了。

观察单个任务状态:LiveData 的妙用

WorkManager 提供了 getWorkInfoByIdLiveData() 方法。它返回一个 LiveData<WorkInfo>,你只要观察它,状态变化就会自动通知你。

举个例子:

val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
    .build()

WorkManager.getInstance(context).enqueue(workRequest)

WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(workRequest.id)
    .observe(this, Observer { workInfo ->
        if (workInfo != null) {
            when (workInfo.state) {
                WorkInfo.State.ENQUEUED -> Log.d("TAG", "任务已排队")
                WorkInfo.State.RUNNING -> Log.d("TAG", "任务正在执行")
                WorkInfo.State.SUCCEEDED -> Log.d("TAG", "任务成功")
                WorkInfo.State.FAILED -> Log.d("TAG", "任务失败")
                WorkInfo.State.CANCELLED -> Log.d("TAG", "任务已取消")
            }
        }
    })

嗯,这里要注意:observe() 的第一个参数是 LifecycleOwner。如果你在 Activity 或 Fragment 里用,直接传 this 就行。它会自动在界面销毁时取消观察,不会造成内存泄漏。

💡 小技巧: 如果你只想在任务完成时做一次处理,可以配合 Transformations.map() 或者直接过滤状态。我经常这么写:workInfo.state == WorkInfo.State.SUCCEEDED 时才触发 UI 更新。

观察多个任务状态:Foreach 与链式任务

有时候你需要同时观察多个任务。比如批量上传图片,每张图片都是一个独立任务。这时候 getWorkInfosByTagLiveData() 就派上用场了。

我习惯给同一类任务打上相同的标签:

val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>()
    .addTag("upload")
    .build()

WorkManager.getInstance(context).enqueue(uploadWork)

WorkManager.getInstance(context)
    .getWorkInfosByTagLiveData("upload")
    .observe(this, Observer { workInfos ->
        workInfos.forEach { workInfo ->
            Log.d("TAG", "任务 ${workInfo.id} 状态: ${workInfo.state}")
        }
    })

你看,workInfos 是一个列表,用 forEach 遍历每个任务的状态。这在批量场景下特别实用。

还有一种情况是链式任务。比如先下载、再解压、最后通知用户。你可以观察整个链的最后一个任务:

val chain = WorkManager.getInstance(context)
    .beginWith(downloadWork)
    .then(unzipWork)
    .then(notifyWork)

chain.enqueue()

chain.workInfosLiveData.observe(this, Observer { workInfos ->
    // workInfos 包含链中所有任务的状态
})
⚠️ 注意: 链式任务的 workInfosLiveData 返回的是 LiveData<List<WorkInfo>>,每次任何一个任务状态变化,整个列表都会重新发射。如果你只关心最后一个任务,建议单独观察它的 ID。

状态转换图:ENQUEUED → RUNNING → SUCCEEDED/FAILED/CANCELLED

理解了怎么观察,我们还得知道状态是怎么流转的。说白了,一个任务的生命周期就是下面这张图:

WorkManager 任务状态转换图 ENQUEUED RUNNING SUCCEEDED FAILED CANCELLED 条件满足 doWork() 返回 Result.success() doWork() 返回 Result.failure() WorkManager.cancelWorkById() 注意:CANCELLED 状态只能从 ENQUEUED 或 RUNNING 进入,不能从 SUCCEEDED/FAILED 回退

这张图我建议你收藏。每次写 Worker 的时候,对照着看,思路会特别清晰。

简单总结一下:

  • ENQUEUED:任务已提交,等待执行。此时可能受约束条件(如网络、电量)影响。
  • RUNNING:任务正在执行。你的 doWork() 方法正在跑。
  • SUCCEEDED:任务成功完成。对应 Result.success()
  • FAILED:任务执行失败。对应 Result.failure()
  • CANCELLED:任务被取消。可以手动调用 cancelWorkById() 触发。
🔑 核心要点: 状态是单向流动的。一旦进入 SUCCEEDED 或 FAILED,就不会再变回 RUNNING。CANCELLED 只能从 ENQUEUED 或 RUNNING 进入。记住这个规则,调试时能省很多时间。

避坑指南:我曾经踩过的坑

我曾经在观察任务状态时,忘记检查 workInfo 是否为 null。结果在任务还没开始的时候,直接访问 workInfo.state 导致空指针。后来我养成了习惯:每次观察都先判空。

还有一次,我在 Fragment 里观察任务状态,但 Fragment 已经销毁了,观察者还在。虽然 LiveData 会自动清理,但如果你用了 observeForever(),一定要在 onDestroy() 里手动移除。不然会出现内存泄漏。

嗯,这些细节看起来小,但真出了问题,排查起来很头疼。

小结

这一节我们聊了 WorkInfo 的结构、怎么用 LiveData 观察单个和多个任务状态,以及状态转换的完整流程。说白了,掌握了这些,你就能实时掌握后台任务的“脉搏”。

下一节,我们会深入 Worker 的内部,聊聊 doWork() 的返回值到底怎么影响任务链。到时候见。

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