第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。

协程与LiveData的四种适用场景流程 耗时任务 (IO) withContext(Main) LiveData.setValue() UI更新 (Main) LiveData.observe() 自动主线程UI 数据流 (Flow) asLiveData() 连续更新UI 错误处理 try/catch LiveData.Error 错误状态统一分发

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
    }
}
部署检查清单:
  1. ViewModel 是否使用 viewModelScope 而非 GlobalScope?
  2. LiveData 观察是否传入生命周期 owner?
  3. Flow 转换时是否在 .asLiveData() 前指定正确的线程调度器?
  4. 错误状态是否覆盖所有异常路径?

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更新需求。