14. LiveData与协程:viewModelScope的使用、在ViewModel中启动协程、LiveData与Flow的转换
好,咱们今天聊点实战的。LiveData 和协程,这两个东西在 Android 开发里几乎是标配了。但很多同学把它们割裂开用——LiveData 只管 UI 通知,协程只管异步任务。其实它们能配合得非常好,尤其是在 ViewModel 里。
我个人习惯是:能用协程的地方,尽量不用回调。LiveData 本身不直接支持协程,但 ViewModel 给了我们一个神器——viewModelScope。有了它,协程的生命周期就和 ViewModel 绑定了,再也不用担心泄漏问题。
14.1 viewModelScope 的正确打开方式
先看个最简单的例子。假设我们要从网络加载用户信息,然后通过 LiveData 通知 UI。
class UserViewModel : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> get() = _user
fun loadUser(userId: String) {
// 在 viewModelScope 中启动协程
viewModelScope.launch {
try {
val result = userRepository.fetchUser(userId) // 挂起函数
_user.value = result
} catch (e: Exception) {
_user.value = User.EMPTY // 出错时给个默认值
}
}
}
}
嗯,这里要注意:viewModelScope.launch 默认运行在 Dispatchers.Main 上。所以你在协程里直接更新 LiveData 是安全的,不用切线程。但如果你要做耗时操作,比如网络请求或数据库读写,记得用 withContext(Dispatchers.IO) 切一下。
我曾经在项目里见过一个坑:有人在 ViewModel 里自己创建了一个 CoroutineScope,然后忘了在 onCleared() 里取消它。结果 Activity 销毁了,协程还在跑,甚至还在更新 LiveData——直接崩溃。用 viewModelScope 就完全避免了这个问题。
14.2 在 ViewModel 中启动协程的几种姿势
除了 launch,我们还有 async。什么时候用哪个?我简单总结一下:
| 场景 | 推荐方式 | 说明 |
|---|---|---|
| 只做一件事,不需要返回值 | launch | 比如更新数据库、发送日志 |
| 需要并发执行多个任务并汇总结果 | async + await | 比如同时请求用户信息和配置信息 |
| 任务之间有依赖关系 | launch 中顺序调用 | 先登录,再获取用户详情 |
来看一个 async 的例子:
class DashboardViewModel : ViewModel() {
private val _dashboardData = MutableLiveData<DashboardData>()
val dashboardData: LiveData<DashboardData> get() = _dashboardData
fun loadDashboard() {
viewModelScope.launch {
// 并发请求两个接口
val userDeferred = async(Dispatchers.IO) { userRepo.getUser() }
val statsDeferred = async(Dispatchers.IO) { statsRepo.getStats() }
// 等待两个结果都回来
val user = userDeferred.await()
val stats = statsDeferred.await()
_dashboardData.value = DashboardData(user, stats)
}
}
}
这里有个小技巧:async 默认也是 Dispatchers.Main,但如果你不指定 Dispatchers.IO,它会在主线程上执行挂起函数——虽然挂起函数不会阻塞主线程,但如果你在里面做了 CPU 密集型操作,还是会卡 UI。所以我习惯在 async 里显式指定调度器。
14.3 LiveData 与 Flow 的转换
说实话,LiveData 在简单场景下很好用,但一旦涉及到复杂的数据流操作(比如 map、filter、combine),它就有点力不从心了。这时候 Flow 就派上用场了。
但问题是,UI 层往往还是习惯用 LiveData。怎么办?答案是:在 ViewModel 里用 Flow 做数据处理,最后转成 LiveData 给 UI 用。
Kotlin 提供了两个扩展方法:
asLiveData():把 Flow 转成 LiveDataasFlow():把 LiveData 转成 Flow
来看一个实际例子。假设我们要监听数据库变化,然后做数据转换:
class ProductViewModel : ViewModel() {
private val productDao: ProductDao = // 注入
// 方式1:直接从 Flow 转成 LiveData
val products: LiveData<List<Product>> = productDao.getAllProducts()
.map { list -> list.filter { it.isActive } } // 过滤掉不活跃的商品
.asLiveData()
// 方式2:如果需要更复杂的逻辑,可以先用 Flow 处理
private val _searchQuery = MutableStateFlow("")
val searchResults: LiveData<List<Product>> = _searchQuery
.debounce(300) // 防抖,避免频繁搜索
.flatMapLatest { query ->
productDao.searchProducts("%$query%")
}
.asLiveData()
fun search(query: String) {
_searchQuery.value = query
}
}
我个人非常喜欢这种模式。Flow 负责数据管道,LiveData 负责 UI 绑定。各司其职,清晰明了。
asLiveData(),记得在 build.gradle 里添加依赖:androidx.lifecycle:lifecycle-livedata-ktx。不然编译器会报找不到这个扩展函数。
14.4 避坑指南:LiveData 与 Flow 转换时的生命周期问题
嗯,这里有个坑我得说一下。我曾经在项目里遇到过一个奇怪的问题:页面退出了再进来,数据不刷新了。
排查了半天,发现是因为 asLiveData() 默认的 timeoutInMs 参数。这个参数的意思是:当没有观察者时,Flow 会延迟多久才取消。默认值是 5000 毫秒。
什么意思呢?就是说你退出页面后,Flow 还会继续跑 5 秒。如果 5 秒内你重新进入页面,它会复用之前的 Flow,不会重新订阅。这在某些场景下会导致数据不是最新的。
解决办法也很简单:
// 设置 timeout 为 0,每次重新订阅都重新触发 Flow
val products: LiveData<List<Product>> = productDao.getAllProducts()
.asLiveData(timeoutInMs = 0)
当然,不是所有场景都需要设成 0。如果你的数据变化不频繁,保留默认值反而能省掉一次数据库查询。看业务需求来定。
14.5 知识体系总览
下面这张图总结了本章的核心内容,我把它画成了 SVG,方便你保存和回顾:
14.6 总结一下
说白了,LiveData 和协程不是二选一的关系,而是互补的。ViewModel 给了我们 viewModelScope,让我们能安全地启动协程;而 Flow 和 LiveData 之间的转换,让我们既能享受 Flow 强大的操作符,又能保持 LiveData 在 UI 层的简洁性。
我个人建议的实践路径是:
- 简单场景:直接用 LiveData + viewModelScope.launch
- 复杂数据流:用 Flow 做数据处理,最后
asLiveData()给 UI - 需要并发:用
async并行请求,再合并结果 - 注意生命周期:理解
asLiveData()的 timeout 参数,避免数据不刷新
记住一点:工具是死的,思路是活的。LiveData 和 Flow 各有优劣,关键看你的场景适合哪个。别为了用 Flow 而用 Flow,也别死守着 LiveData 不放。灵活搭配,才是高手。
viewModelScope。它只能在 ViewModel 内部访问。如果你在 UI 层需要启动协程,请使用 lifecycleScope。