17、任务进度更新:setProgress()方法、观察进度(WorkInfo.getProgress)、在UI中展示进度条、进度更新的最佳实践
大家好,我是你们的老朋友。今天我们来聊聊WorkManager里一个非常实用的功能——任务进度更新。
说实话,我在早期做后台任务时,经常遇到一个尴尬的场景:用户点击了「开始下载」,然后界面就卡在那里一动不动。用户不知道任务是在进行中,还是已经卡死了。这种体验,说白了就是「黑盒操作」,用户心里没底。
后来我引入了WorkManager的进度更新机制,才真正解决了这个问题。今天我就把这块内容掰开揉碎了讲给你听。
17.1 setProgress()方法:给任务装上「仪表盘」
先说说核心方法——setProgress()。这个方法的作用很简单:在Worker执行过程中,向WorkManager报告当前的进度。
它的签名是这样的:
// 在Worker内部调用
setProgress(workDataOf(
"progress" to 0.5f,
"status" to "正在下载..."
))
你可能会问:「为什么不用LiveData或者Flow?」嗯,这里有个设计上的考量。WorkManager的进度数据是持久化的,即使App进程被杀掉,进度信息依然保存在数据库中。等App重新启动,你依然能拿到之前的进度。这一点,普通的LiveData做不到。
我在项目中遇到过这样一个场景:用户下载一个大文件,中途接了个电话,App被系统回收了。等用户再切回来,进度条居然从0%重新开始。用户当场就炸了。后来改用setProgress(),这个问题就彻底解决了。
17.2 观察进度:WorkInfo.getProgress 的正确打开方式
Worker那边报告了进度,UI这边怎么接收呢?答案是——通过WorkInfo。
WorkManager提供了两种观察方式:
- LiveData:适合传统的Android架构
- Kotlin Flow:适合协程风格的代码
先看LiveData的用法:
val workManager = WorkManager.getInstance(context)
workManager.getWorkInfoByIdLiveData(workRequest.id)
.observe(viewLifecycleOwner) { workInfo ->
if (workInfo != null) {
val progress = workInfo.progress
val currentProgress = progress.getFloat("progress", 0f)
val status = progress.getString("status", "")
// 更新UI
}
}
再看Flow的版本,我个人更推荐这个:
workManager.getWorkInfoByIdFlow(workRequest.id)
.collect { workInfo ->
val progress = workInfo.progress
// 处理进度数据
}
这里有个坑,我曾经踩过:workInfo.progress返回的是一个Data对象,不是直接的数字。你需要用getFloat()、getString()这些方法去提取。如果你忘了做空判断,直接调用getFloat("progress", 0f)的默认值参数,那就会拿到0。嗯,这会导致进度条一直停在0%,用户会以为任务没开始。
17.3 在UI中展示进度条:从数据到视觉
拿到进度数据后,展示到UI上就简单了。我一般用ProgressBar或者MaterialProgressBar。
一个典型的实现是这样的:
// 在Fragment或Activity中
private fun bindProgress(workInfo: WorkInfo) {
val progress = workInfo.progress
val currentProgress = progress.getFloat("progress", 0f)
val status = progress.getString("status", "")
binding.progressBar.progress = (currentProgress * 100).toInt()
binding.statusText.text = status
// 任务完成时隐藏进度条
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
binding.progressBar.visibility = View.GONE
binding.statusText.text = "任务完成"
}
}
你想想看,用户看到进度条在动,心里就有底了。哪怕任务执行了30秒,用户也愿意等。这就是「感知性能」的重要性。
17.4 进度更新的最佳实践:我踩过的坑和总结的经验
这部分是我最想跟你分享的。毕竟光知道API怎么用还不够,真正落地时会有各种细节问题。
17.4.1 更新频率控制
我在项目中遇到过一个问题:Worker里每处理一条数据就调用一次setProgress()。结果呢?UI线程被频繁刷新,界面卡顿。更糟糕的是,WorkManager的数据库写入操作也很频繁,反而拖慢了任务本身。
解决方案很简单——控制更新频率:
// 每处理10条数据,或者每过500ms,才更新一次进度
var lastUpdateTime = 0L
var processedCount = 0
for (item in dataList) {
processItem(item)
processedCount++
val currentTime = System.currentTimeMillis()
if (processedCount % 10 == 0 || currentTime - lastUpdateTime > 500) {
val progress = processedCount.toFloat() / dataList.size
setProgress(workDataOf("progress" to progress))
lastUpdateTime = currentTime
}
}
17.4.2 进度数据的结构化
我习惯把进度数据封装成一个固定的结构,而不是随意拼字段。这样UI侧解析起来也统一:
object ProgressKeys {
const val PROGRESS = "progress" // 0.0 ~ 1.0
const val STATUS = "status" // 文本状态
const val TOTAL = "total" // 总任务量
const val CURRENT = "current" // 当前完成量
}
// Worker中
setProgress(workDataOf(
ProgressKeys.PROGRESS to 0.75f,
ProgressKeys.STATUS to "正在压缩图片...",
ProgressKeys.TOTAL to 100,
ProgressKeys.CURRENT to 75
))
17.4.3 处理任务取消时的进度
当用户取消任务时,WorkManager会调用onStopped()方法。这时候你应该把进度标记为「已取消」:
override fun onStopped() {
super.onStopped()
setProgress(workDataOf(
"progress" to 0f,
"status" to "任务已取消"
))
}
这样UI侧就能及时感知到任务被取消,并更新界面。
17.4.4 不要忘记清理观察者
这个其实是个老生常谈的问题,但我还是想提一下。如果你用LiveData观察进度,记得在View销毁时移除观察者。否则会导致内存泄漏。
用Flow的话,配合viewModelScope或者lifecycleScope,系统会自动管理生命周期。
- setProgress() 是持久化的,App重启后依然有效
- 控制更新频率,别让UI频繁刷新
- 进度数据结构化,方便UI侧统一解析
- 任务取消时也要更新进度状态
- 记得清理观察者,避免内存泄漏
17.5 知识体系总览
下面这张图帮你梳理了本章的核心逻辑:
这张图展示了从Worker执行任务,到最终UI展示进度条的完整链路。核心就是:Worker调用setProgress() → 数据持久化到WorkManager数据库 → UI通过WorkInfo观察进度变化。
好了,关于进度更新的内容就讲到这里。记住一句话:让用户看到进度,就是让用户感到安心。下次你的App再做后台任务,别忘了加上进度条。