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,每一层做什么,一目了然。

DataBinding + WorkManager 数据流架构 Worker setProgress() doWork() 持久化 WorkManager Room 数据库 状态管理 LiveData ViewModel Transformations.map 状态转换 绑定 DataBinding (XML) @{} 表达式 自动更新 UI 观察 关键点: 1. Worker 通过 setProgress() 更新进度,WorkManager 持久化到 Room 2. ViewModel 通过 getWorkInfoByIdLiveData() 获取 LiveData 3. DataBinding 在 XML 中直接绑定 ViewModel 暴露的 LiveData

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