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会自动结束,不会内存泄漏。

核心要点:WorkManager的Flow API本质上是把内部的LiveData转换成了Flow,但它保留了生命周期感知的特性。你可以在ViewModel里用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()本身是线程安全的,但频繁调用确实会影响性能。

我的建议:如果你的Worker需要频繁更新进度(比如每1%更新一次),可以考虑在Worker内部用一个ChannelMutableStateFlow来缓冲,然后定时批量调用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刷新。

注意:WorkManager的进度数据是持久化到Room数据库的。这意味着即使App进程被杀,进度数据也不会丢失。但反过来,如果你在Worker里更新了进度,而UI层没有及时collect,那么下次collect时会拿到最新的进度值。这通常是我们期望的行为,但如果你需要“增量”进度,就得自己维护一个偏移量了。

SVG流程图:Flow + WorkManager 数据流

下面这张图展示了从Worker到UI的完整数据流。我把它画成了三层结构,方便你理解每个环节的职责。

Flow + WorkManager 数据流架构 Worker 层(后台执行) CoroutineWorker.doWork() setProgress() 多次调用 Room 持久化存储 Flow 层(数据管道) getWorkInfoByIdFlow() .map { progress } .distinctUntilChanged() UI 层(展示与交互) ViewModel.collect() StateFlow 暴露给UI Compose/View 渲染 数据从Worker产生,经过Flow管道转换,最终到达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实例。
小技巧:如果你在Worker里需要发射多个不同类型的进度(比如“正在压缩”、“正在上传”、“上传完成”),可以用一个密封类作为进度类型,然后序列化到Data里。这样UI层可以根据类型展示不同的文案和进度条。

好了,这一章的内容就到这里。Flow和WorkManager的结合,说白了就是把“命令式”的后台任务变成了“响应式”的数据流。你不需要再手动轮询数据库,也不需要写一堆回调接口。只要把Worker的进度通过setProgress()吐出来,然后用Flow在UI层优雅地消费它。嗯,这就是我理解的“响应式后台任务”。

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