21、LiveData与WorkManager:WorkManager返回LiveData、监听后台任务状态、实战:文件下载进度

后台任务管理,一直是Android开发里的老大难问题。我记得早期做项目时,为了在后台下载文件,各种Service、BroadcastReceiver轮番上阵,还得自己处理屏幕旋转、进程被杀这些场景,代码写得那叫一个痛苦。后来Google推出了WorkManager,我才算真正解脱了。

这一章,我们就来聊聊WorkManager和LiveData怎么配合。说白了,就是让后台任务的状态变化,能实时通知到UI层。你想想看,用户点了个下载按钮,然后界面上的进度条能跟着动,这种体验多自然。

WorkManager的基本认知

WorkManager不是普通的后台任务工具。它是Google官方推荐的、用于处理可延迟后台任务的库。什么叫可延迟?就是那些不需要立即执行、但保证最终会执行的任务。比如上传日志、同步数据、定期清理缓存,当然也包括我们今天要讲的文件下载。

它的核心优势有几个:

  • 兼容性强:API 14以上都能用,内部会根据系统版本自动选择不同的实现方式
  • 生命周期感知:任务状态变化可以通知给观察者,哪怕App被杀了也能恢复
  • 链式调用:多个任务可以串联或并联执行

我个人习惯把WorkManager看作是「系统级的任务调度器」。它不关心你的任务跑在哪个线程,只关心任务有没有完成、什么时候该重试。

WorkManager返回LiveData

WorkManager天生就支持LiveData。它提供了一个方法叫getWorkInfoByIdLiveData(),你传进去一个任务ID,它返回一个LiveData对象。这个LiveData会随着任务状态的变化而自动更新。

来看一段代码:

// 启动一个后台任务
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
    .setInputData(workDataOf("url" to "https://example.com/file.zip"))
    .build()

WorkManager.getInstance(context).enqueue(workRequest)

// 监听任务状态
val workId = workRequest.id
WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(workId)
    .observe(this, Observer { workInfo ->
        if (workInfo != null) {
            val state = workInfo.state
            when (state) {
                WorkInfo.State.RUNNING -> {
                    // 任务正在执行
                    val progress = workInfo.progress
                    val percent = progress.getInt("progress", 0)
                    // 更新UI
                }
                WorkInfo.State.SUCCEEDED -> {
                    // 任务完成
                }
                WorkInfo.State.FAILED -> {
                    // 任务失败
                }
                else -> { /* 其他状态 */ }
            }
        }
    })

这里有个细节要注意:getWorkInfoByIdLiveData返回的LiveData是粘性的。什么意思?就是如果你在任务已经开始后才去观察,它会把当前最新的状态发给你。嗯,这个特性在页面重建时特别有用。

小提示:如果你需要同时监听多个任务,可以用getWorkInfosByTagLiveData()或者getWorkInfosForUniqueWorkLiveData()。我个人更推荐用tag来管理,因为tag可以给一组任务打上相同的标签,方便统一控制。

监听后台任务状态

WorkManager的任务状态其实是一个有限状态机。我画了一张图,你可以直观地看到状态之间的流转关系:

ENQUEUED RUNNING SUCCEEDED FAILED CANCELLED 等待执行 正在执行 执行成功 执行失败 已取消

这张图展示了WorkManager的完整状态流转。从ENQUEUED开始,进入RUNNING,然后要么SUCCEEDED,要么FAILED。任何时候都可以CANCELLED。注意,FAILED之后不会自动重试,除非你设置了重试策略。

我在项目中遇到过一个问题:任务明明失败了,但UI上却显示还在运行。后来发现是因为没有正确处理FAILED状态。所以这里要提醒大家,when表达式一定要覆盖所有状态分支,尤其是else分支。

注意:WorkManager的状态是幂等的。同一个任务不会从SUCCEEDED变成FAILED。如果你需要重新执行,必须创建一个新的WorkRequest。

实战:文件下载进度

好了,理论说完了,我们来写一个真正的文件下载功能。这个例子我会把LiveData、ViewModel和WorkManager串起来用。

先定义一个Worker:

class DownloadWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        val url = inputData.getString("url") ?: return Result.failure()
        
        try {
            val connection = URL(url).openConnection() as HttpURLConnection
            connection.connect()
            
            val fileLength = connection.contentLength
            val inputStream = connection.inputStream
            val outputStream = FileOutputStream(
                File(applicationContext.filesDir, "download.zip")
            )
            
            val buffer = ByteArray(8192)
            var bytesRead: Int
            var totalBytesRead = 0L
            
            while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                outputStream.write(buffer, 0, bytesRead)
                totalBytesRead += bytesRead
                
                // 更新进度
                val progress = if (fileLength > 0) {
                    (totalBytesRead * 100 / fileLength).toInt()
                } else {
                    0
                }
                
                setProgress(workDataOf("progress" to progress))
            }
            
            outputStream.close()
            inputStream.close()
            
            return Result.success()
        } catch (e: Exception) {
            return Result.failure()
        }
    }
}

这段代码里,我用了setProgress()来更新进度。这个进度数据会通过LiveData暴露出去。注意,setProgress()只能在doWork()内部调用,而且调用频率不要太高,否则会有性能问题。

接下来是ViewModel层:

class DownloadViewModel : ViewModel() {
    
    private val _downloadState = MutableLiveData<DownloadState>()
    val downloadState: LiveData<DownloadState> = _downloadState
    
    private var workId: UUID? = null
    
    fun startDownload(url: String) {
        val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
            .setInputData(workDataOf("url" to url))
            .build()
        
        workId = workRequest.id
        
        WorkManager.getInstance(getApplication())
            .enqueue(workRequest)
        
        // 监听进度
        WorkManager.getInstance(getApplication())
            .getWorkInfoByIdLiveData(workRequest.id)
            .observeForever { workInfo ->
                if (workInfo != null) {
                    val progress = workInfo.progress.getInt("progress", 0)
                    when (workInfo.state) {
                        WorkInfo.State.RUNNING -> {
                            _downloadState.value = DownloadState.Downloading(progress)
                        }
                        WorkInfo.State.SUCCEEDED -> {
                            _downloadState.value = DownloadState.Success
                        }
                        WorkInfo.State.FAILED -> {
                            _downloadState.value = DownloadState.Error("下载失败")
                        }
                        else -> { /* 不做处理 */ }
                    }
                }
            }
    }
    
    fun cancelDownload() {
        workId?.let {
            WorkManager.getInstance(getApplication()).cancelWorkById(it)
        }
    }
}

sealed class DownloadState {
    data class Downloading(val progress: Int) : DownloadState()
    object Success : DownloadState()
    data class Error(val message: String) : DownloadState()
}

这里我用了一个sealed class来定义下载状态。为什么不用普通的枚举?因为Downloading需要携带进度值,而sealed class天然支持这种带参数的状态。这是我个人比较喜欢的一种写法。

最后是Activity或Fragment里的使用:

viewModel.downloadState.observe(this) { state ->
    when (state) {
        is DownloadState.Downloading -> {
            progressBar.progress = state.progress
            textView.text = "${state.progress}%"
        }
        is DownloadState.Success -> {
            Toast.makeText(this, "下载完成", Toast.LENGTH_SHORT).show()
        }
        is DownloadState.Error -> {
            Toast.makeText(this, state.message, Toast.LENGTH_SHORT).show()
        }
    }
}

你看,整个流程非常清晰。ViewModel负责调度任务和暴露状态,Activity只负责展示。哪怕屏幕旋转了,下载任务也不会中断,因为WorkManager是独立于Activity生命周期的。

核心要点:
  • WorkManager + LiveData = 后台任务状态实时通知
  • 使用setProgress()更新进度,频率控制在每秒几次即可
  • ViewModel中通过observeForever监听,注意在onCleared()中移除观察者
  • sealed class定义状态,比枚举更灵活

我曾经在一个电商项目里用过这套方案。当时需要批量下载商品图片,用户可以在下载过程中随时取消。用WorkManager配合LiveData,代码量比之前用Service少了将近一半,而且稳定性好很多。嗯,这就是我为什么推荐它的原因。

最后说一句:WorkManager不是万能的。如果你的任务需要立即执行、或者需要在前台显示通知,那还是得用Foreground Service。但对于大多数后台任务来说,WorkManager + LiveData的组合,已经足够优雅了。


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