第3章 协程与LiveData的适用场景:耗时任务、UI更新、数据流、错误处理
3.1 场景概述
协程提供异步任务管理能力,LiveData负责生命周期感知的数据分发。两者结合时,需要明确每个场景下谁负责“触发异步”、谁负责“通知观察者”、谁负责“处理边界条件”。本章从四个核心场景出发,给出对应的代码模式、配置要点和常见陷阱。
| 场景 | 协程角色 | LiveData角色 | 关键API |
|---|---|---|---|
| 耗时任务(网络/IO/计算) | 在后台线程执行 | 通知UI任务完成 | viewModelScope.launch(Dispatchers.IO) |
| UI更新 | 确保发布在主线程 | 自动粘性更新 | .postValue() / .setValue() |
| 数据流(连续变化) | 生产数据流(Flow) | 转换为LiveData供UI消费 | flow { … } + .asLiveData() |
| 错误处理 | 捕获并传递异常 | 承载错误状态 | try/catch → liveDataError.value |
3.2 耗时任务:后台执行 + 结果通知
最基础的混合使用:在 ViewModel 中通过协程执行耗时操作,执行完毕后使用 LiveData 更新UI。
实现原则: 耗时操作必须在
Dispatchers.IO 或 Default 上执行,LiveData 的 setValue() 只能在主线程调用,postValue() 可在任何线程调用。
// ViewModel 片段
class DataViewModel : ViewModel() {
private val _result = MutableLiveData<String>()
val result: LiveData<String> = _result
fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
// 模拟耗时网络请求
val data = networkService.getData() // 挂起函数
// 切换到主线程更新 LiveData
withContext(Dispatchers.Main) {
_result.value = data
}
}
}
}
调试技巧: 使用
Thread.currentThread().name 在关键点打印线程名,确认 IO 与 Main 切换正确。
3.3 UI更新:生命周期安全 + 粘性分发
LiveData 自动感知 LifecycleOwner(如 Activity/Fragment)的销毁,避免内存泄漏;同时协程通过 viewModelScope 自动取消。UI 更新无需手动管理订阅/取消。
// Activity 中观察 LiveData
viewModel.result.observe(this) { data ->
// 仅在前台时执行,自动处理生命周期
textView.text = data
}
常见错误: 在协程内部直接调用
LiveData.setValue() 而未切换到主线程,会抛 IllegalStateException。强制使用 postValue() 或 withContext(Dispatchers.Main)。
3.4 数据流:Flow 转 LiveData
当数据持续变化(如传感器读数、定时更新)时,使用协程 Flow 生产数据,再用 asLiveData() 扩展函数转换为 LiveData。
// build.gradle.kts (Module)
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
// ViewModel
class SensorViewModel : ViewModel() {
val sensorData: LiveData<Int> = flow {
var counter = 0
while (true) {
delay(1000)
emit(counter++)
}
}.asLiveData()
}
Flow 的冷流特性确保只有观察者存在时才启动生产,asLiveData() 内部自动处理协程生命周期。
3.5 错误处理:统一错误状态
用 sealed class 定义加载、成功、错误三种状态,通过 LiveData 分发,协程中捕获异常并更新状态。
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
// ViewModel
private val _uiState = MutableLiveData<UiState<String>>()
val uiState: LiveData<UiState<String>> = _uiState
fun loadData() {
_uiState.value = UiState.Loading
viewModelScope.launch(Dispatchers.IO) {
try {
val result = riskyOperation() // 可能抛异常
withContext(Dispatchers.Main) {
_uiState.value = UiState.Success(result)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
_uiState.value = UiState.Error(e.message ?: "未知错误")
}
}
}
}
稳定性优化: 使用
CoroutineExceptionHandler 注册全局未捕获异常日志,避免静默吞异常。
3.6 四种场景的流程融合示意图
下图展示协程与 LiveData 在四个场景中的交互关系:耗时任务从 IO 切回 Main,UI 更新由 LiveData 分发,数据流通过 Flow 转换,错误处理经 try/catch 进入 LiveData。
3.7 性能与稳定性要点
- 避免重复创建协程: 在
ViewModel中每次调用方法时检查_uiState.value是否为Loading,防止重复请求。 - 内存泄漏防范: 不要在协程中持有 Activity 引用;使用
viewModelScope或lifecycleScope自动取消。 - 数据一致性:
postValue()可能合并连续更新,若需要每个中间值可见,使用setValue()并确保在主线程调用。 - 异常边界: 对所有挂起函数包裹
try/catch,避免协程内未处理异常导致崩溃。
3.8 调试与日志建议
// 在协程启动和结束时添加日志
viewModelScope.launch(Dispatchers.IO) {
Log.d("CoroutineLiveData", "开始耗时任务 on ${Thread.currentThread().name}")
val result = fetchData()
withContext(Dispatchers.Main) {
Log.d("CoroutineLiveData", "更新LiveData on ${Thread.currentThread().name}")
_result.value = result
}
}
部署检查清单:
- ViewModel 是否使用
viewModelScope而非GlobalScope? - LiveData 观察是否传入生命周期 owner?
- Flow 转换时是否在
.asLiveData()前指定正确的线程调度器? - 错误状态是否覆盖所有异常路径?
3.9 常见错误速查
| 错误现象 | 根本原因 | 解决方案 |
|---|---|---|
setValue called from background thread |
在非主线程调用了 setValue() |
改为 postValue() 或 withContext(Main) |
| LiveData 不更新 | 未执行 observe() 或 Lifecycle 已 Destroyed |
检查 activity 是否有效,或使用 observeForever()(需手动移除) |
| Flow 不发射数据 | asLiveData() 前未使用 flowOn(Dispatchers.Default) |
显式指定 flowOn() 到后台调度器 |
| 协程未取消导致内存泄漏 | 使用了 GlobalScope 或未绑定 viewModelScope |
统一使用 lifecycleScope 或 viewModelScope |
掌握上述四个场景的模式后,可以应对绝大多数 Android 开发中的异步UI更新需求。