15、DataBinding与WorkManager:WorkManager状态绑定、进度更新、链式任务绑定、周期性任务绑定
说实话,DataBinding 和 WorkManager 这两个东西,单独拎出来大家可能都熟悉。但把它们组合在一起用,很多人就懵了。
我刚开始做这个组合的时候也踩过坑。WorkManager 跑在后台,DataBinding 管着 UI,怎么让它们俩「对话」?说白了,就是怎么把后台任务的进度、状态实时反映到界面上。
这一章,我们就来彻底搞定这件事。
15.1 为什么需要把 WorkManager 和 DataBinding 结合?
你想想看,WorkManager 处理后台任务,比如上传文件、同步数据。用户看着屏幕,总得知道「传到哪了」吧?
传统做法是写一堆回调、Handler、LiveData。代码又臭又长。DataBinding 的出现,让我们可以直接在 XML 里绑定数据。那 WorkManager 的状态,能不能也直接绑进去?
当然可以。核心思路就一句话:把 WorkManager 的观察对象(比如 LiveData)暴露给 DataBinding。
核心思想: WorkManager 提供状态和进度的 LiveData,ViewModel 持有并转换这些 LiveData,DataBinding 在 XML 中直接绑定 ViewModel 暴露的 LiveData。
15.2 WorkManager 状态绑定
先看最简单的场景:监听一个后台任务的状态。是成功、失败,还是正在运行?
15.2.1 基础状态监听
WorkManager 的 getWorkInfoByIdLiveData() 会返回一个 LiveData。我们直接在 ViewModel 里把它暴露出去。
// ViewModel 中
class MyViewModel : ViewModel() {
private val workManager = WorkManager.getInstance(application)
// 暴露给 DataBinding 的状态
val workStatus: LiveData<WorkInfo> = workManager
.getWorkInfoByIdLiveData(workRequest.id)
// 也可以转换成更友好的状态字符串
val statusText: LiveData<String> = Transformations.map(workStatus) { info ->
when (info?.state) {
WorkInfo.State.ENQUEUED -> "排队中"
WorkInfo.State.RUNNING -> "执行中"
WorkInfo.State.SUCCEEDED -> "已完成"
WorkInfo.State.FAILED -> "失败"
else -> "未知"
}
}
}
然后在 XML 里直接绑定:
<layout>
<data>
<variable name="viewModel" type=".MyViewModel"/>
</data>
<TextView
android:text="@{viewModel.statusText}"
... />
</layout>
嗯,这里要注意:Transformations.map 必须在主线程观察。WorkManager 的 LiveData 默认就是主线程安全的,所以没问题。
个人习惯: 我一般不在 XML 里直接绑定 WorkInfo 对象,而是转换成字符串或枚举。这样 XML 更干净,也方便做国际化。
15.3 进度更新
状态绑定只是第一步。很多时候我们需要显示「已上传 45%」这样的进度。
WorkManager 支持通过 setProgress() 更新进度。DataBinding 这边,我们需要监听 getWorkInfoByIdLiveData() 里的 progress 字段。
15.3.1 Worker 中更新进度
class UploadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val total = 100
for (i in 1..total) {
// 模拟上传
delay(50)
// 更新进度
setProgress(workDataOf("progress" to i))
}
return Result.success()
}
}
15.3.2 ViewModel 中暴露进度
class MyViewModel : ViewModel() {
val progress: LiveData<Int> = Transformations.map(workStatus) { info ->
info?.progress?.getInt("progress", 0) ?: 0
}
// 也可以转成百分比字符串
val progressText: LiveData<String> = Transformations.map(progress) {
"$it%"
}
}
15.3.3 XML 中绑定进度条
<ProgressBar
android:progress="@{viewModel.progress}"
style="?android:attr/progressBarStyleHorizontal"
... />
<TextView
android:text="@{viewModel.progressText}"
... />
我曾经踩过的坑: 进度更新不要太频繁。WorkManager 的进度是通过 Room 数据库持久化的,每秒更新几十次会导致性能问题。我一般控制在每秒 2-3 次,或者每完成 1% 更新一次。
15.4 链式任务绑定
实际项目中,任务往往不是孤立的。比如:先下载 -> 再解压 -> 最后导入。这就是链式任务。
WorkManager 用 beginWith() 和 then() 来构建链。那 DataBinding 怎么绑定整个链的状态?
15.4.1 构建链式任务
val download = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")
.build()
val unzip = OneTimeWorkRequestBuilder<UnzipWorker>()
.addTag("unzip")
.build()
val import = OneTimeWorkRequestBuilder<ImportWorker>()
.addTag("import")
.build()
workManager.beginWith(download)
.then(unzip)
.then(import)
.enqueue()
15.4.2 观察整个链的状态
这里有个技巧:用 getWorkInfosByTagLiveData() 可以拿到所有同标签的任务状态。
class ChainViewModel : ViewModel() {
private val workManager = WorkManager.getInstance(application)
// 观察所有带 "chain" 标签的任务
val chainInfos: LiveData<List<WorkInfo>> =
workManager.getWorkInfosByTagLiveData("chain")
// 当前正在执行的任务名称
val currentTaskName: LiveData<String> = Transformations.map(chainInfos) { infos ->
infos?.find { it.state == WorkInfo.State.RUNNING }?.tags?.firstOrNull() ?: "等待中"
}
// 整体进度(假设每个子任务占 33%)
val overallProgress: LiveData<Int> = Transformations.map(chainInfos) { infos ->
val succeeded = infos?.count { it.state == WorkInfo.State.SUCCEEDED } ?: 0
(succeeded * 100) / 3
}
}
关键点: 链式任务的状态绑定,核心是「聚合观察」。不要为每个子任务单独创建 LiveData,而是用一个 LiveData 观察整个链,然后在 ViewModel 里做拆分和转换。
15.5 周期性任务绑定
周期性任务,比如每天同步一次数据。WorkManager 用 PeriodicWorkRequest 实现。DataBinding 这边,我们需要显示「下次执行时间」或「上次执行结果」。
15.5.1 创建周期性任务
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
24, TimeUnit.HOURS
).build()
workManager.enqueueUniquePeriodicWork(
"sync",
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
15.5.2 绑定下次执行时间
class PeriodicViewModel : ViewModel() {
val nextRunTime: LiveData<String> = Transformations.map(workStatus) { info ->
info?.nextScheduleTimeMillis?.let { millis ->
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
sdf.format(Date(millis))
} ?: "未调度"
}
val lastRunResult: LiveData<String> = Transformations.map(workStatus) { info ->
when (info?.state) {
WorkInfo.State.SUCCEEDED -> "上次成功"
WorkInfo.State.FAILED -> "上次失败"
else -> "运行中"
}
}
}
15.5.3 XML 绑定
<TextView
android:text='@{"下次同步: " + viewModel.nextRunTime}'
... />
<TextView
android:text="@{viewModel.lastRunResult}"
... />
我建议: 周期性任务的状态绑定,最好加上「手动刷新」按钮。因为 WorkManager 的状态更新可能有延迟,用户点一下刷新,能立刻看到最新状态。
15.6 整体架构图
下面这张图,把整个数据流向画清楚了。从 Worker 到 DataBinding,每一层做什么,一目了然。
15.7 避坑指南
最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 进度条不更新 | Worker 中 setProgress 后,LiveData 没有及时触发 | 确保在 doWork 中调用 setProgress,并且 ViewModel 观察的是同一个 workId |
| 链式任务状态混乱 | 多个任务共用同一个 tag,导致状态互相覆盖 | 每个链使用唯一的 tag,或者用 workId 精确观察 |
| 周期性任务不执行 | 系统省电策略限制了 WorkManager | 使用 setConstraints() 设置合适的约束条件,比如网络可用时执行 |
| DataBinding 不刷新 | LiveData 没有在主线程观察 | 确保 ViewModel 中的 LiveData 是从 WorkManager 的主线程安全 API 获取的 |
我曾经犯过的错: 有一次我把 WorkManager 的 LiveData 直接暴露给了 Fragment,然后在 Fragment 里手动转换。结果 Fragment 重建时,观察者丢失了,进度再也不更新。后来我统一在 ViewModel 里做转换,用 DataBinding 自动绑定,再也没出过问题。
说白了,DataBinding 和 WorkManager 的组合,核心就是「ViewModel 作为桥梁」。Worker 只管干活和报进度,ViewModel 负责转换和暴露,DataBinding 负责展示。各司其职,代码自然清爽。
公众号:蓝海资料掘金营,微信deep3321