14、Flow与WorkManager:WorkManager的Flow支持、后台任务的Flow化、进度监听
后台任务处理,一直是Android开发中的重头戏。我个人习惯用WorkManager来处理那些需要保证执行的任务——比如上传日志、同步数据、定期清理缓存。但以前写WorkManager的代码,总感觉有点别扭:你要监听进度?得写一堆回调。你要观察结果?得查数据库。说白了,就是缺一个“响应式”的管道。
好在Google后来给WorkManager加上了Flow支持。嗯,这一章我们就来聊聊,怎么把WorkManager和Flow结合起来,让后台任务也变得“流式”起来。
WorkManager的Flow支持:从回调到流
先说说最基础的。WorkManager本身提供了getWorkInfoByIdLiveData()这样的LiveData方法,但如果你已经全面拥抱Flow,那肯定更希望用getWorkInfoByIdFlow()。这个API是WorkManager 2.7.0之后引入的,它返回的是一个Flow<WorkInfo>。
我在项目中遇到过这样一个场景:用户上传一张图片,我需要实时显示上传进度。如果用LiveData,我得手动在Observer里更新UI;但用Flow,我可以直接配合collect,甚至用StateFlow把状态暴露给Compose或View层。
// 启动一个Worker
val workRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.build()
WorkManager.getInstance(context).enqueue(workRequest)
// 用Flow监听状态
WorkManager.getInstance(context)
.getWorkInfoByIdFlow(workRequest.id)
.filter { it.state == WorkInfo.State.RUNNING }
.map { it.progress }
.collect { progress: Data ->
val percent = progress.getInt("progress", 0)
// 更新UI
}
你想想看,这比写一个Observer然后手动removeObserver要清爽多少?而且Flow天然支持协程取消,页面销毁时collect会自动结束,不会内存泄漏。
viewModelScope来collect,完全不用担心泄漏。
后台任务的Flow化:把Worker变成数据源
接下来是重头戏。我们不仅要监听WorkManager的状态,还要让Worker本身“流式”地输出数据。说白了,就是让Worker在执行过程中,不断地向外发射进度或中间结果。
WorkManager提供了setProgress()方法,你可以在Worker的doWork()里多次调用它。但问题是,setProgress()是同步的,而且它把数据存到了Room数据库里。如果你在Worker里做耗时操作,比如分块上传一个大文件,你可以在每上传完一块后调用一次setProgress()。
class UploadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val totalParts = 10
for (i in 1..totalParts) {
// 模拟上传一块数据
delay(1000)
// 更新进度
setProgress(workDataOf("progress" to (i * 100 / totalParts)))
}
return Result.success()
}
}
然后在外层,我们用Flow来收集这个进度。这里有个小技巧:getWorkInfoByIdFlow()每次发射的是完整的WorkInfo对象,而WorkInfo里的progress字段就是最新的进度数据。所以你可以这样写:
fun observeProgress(workId: UUID): Flow<Int> {
return WorkManager.getInstance(context)
.getWorkInfoByIdFlow(workId)
.map { workInfo ->
workInfo.progress.getInt("progress", 0)
}
.distinctUntilChanged() // 避免重复值
}
我曾经踩过一个坑:setProgress()调用太频繁,导致Room数据库写入压力大。后来我加了一个节流策略——每200毫秒才更新一次进度。嗯,这里要注意,setProgress()本身是线程安全的,但频繁调用确实会影响性能。
Channel或MutableStateFlow来缓冲,然后定时批量调用setProgress()。这样既保证了UI的实时性,又不会把数据库写爆。
进度监听的完整实践:从Worker到UI
光有Worker端的进度输出还不够,我们得在UI层优雅地消费它。我个人习惯在ViewModel里做这件事,因为ViewModel的生命周期和屏幕旋转无关。
class UploadViewModel(private val workManager: WorkManager) : ViewModel() {
private val _progress = MutableStateFlow(0)
val progress: StateFlow<Int> = _progress.asStateFlow()
private var currentWorkId: UUID? = null
fun startUpload() {
val workRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.build()
currentWorkId = workRequest.id
workManager.enqueue(workRequest)
// 收集进度
viewModelScope.launch {
workManager.getWorkInfoByIdFlow(workRequest.id)
.map { it.progress.getInt("progress", 0) }
.distinctUntilChanged()
.collect { p ->
_progress.value = p
}
}
}
fun cancelUpload() {
currentWorkId?.let { workManager.cancelWorkById(it) }
}
}
然后在Compose或View层,直接观察这个StateFlow即可。如果你用的是Compose,可以这样:
@Composable
fun UploadScreen(viewModel: UploadViewModel) {
val progress by viewModel.progress.collectAsState()
LinearProgressIndicator(progress = progress / 100f)
}
这里有个细节:distinctUntilChanged()很重要。因为getWorkInfoByIdFlow()在Worker状态变化时也会发射数据,即使进度没变。加上这个操作符,可以避免不必要的UI刷新。
SVG流程图:Flow + WorkManager 数据流
下面这张图展示了从Worker到UI的完整数据流。我把它画成了三层结构,方便你理解每个环节的职责。
避坑指南与最佳实践
最后,我总结几个实际项目中容易踩的坑,以及我个人的处理方式。
- 进度更新频率控制:我曾经在一个文件上传Worker里每上传1KB就调用一次
setProgress(),结果导致Room写入队列暴涨,UI卡顿。后来改成每5%更新一次,问题解决。建议根据任务粒度合理设置更新间隔。 - Worker被取消后的处理:如果用户取消了任务,
getWorkInfoByIdFlow()会发射一个CANCELLED状态的WorkInfo。你需要在collect时处理这个状态,比如重置进度条。我一般用filter { it.state == WorkInfo.State.RUNNING }来过滤掉非运行状态。 - 多个Worker的进度合并:如果你同时启动了多个Worker(比如批量上传图片),可以用
combine操作符合并它们的进度。每个Worker的进度作为一个Flow,然后计算平均值或总和。 - 测试问题:WorkManager的Flow API在测试时有点麻烦,因为它依赖Room数据库。我建议用
TestListenableWorkerBuilder来模拟Worker,然后直接测试Flow的发射逻辑,而不是依赖真实的WorkManager实例。
Data里。这样UI层可以根据类型展示不同的文案和进度条。
好了,这一章的内容就到这里。Flow和WorkManager的结合,说白了就是把“命令式”的后台任务变成了“响应式”的数据流。你不需要再手动轮询数据库,也不需要写一堆回调接口。只要把Worker的进度通过setProgress()吐出来,然后用Flow在UI层优雅地消费它。嗯,这就是我理解的“响应式后台任务”。