14. LiveData与协程:viewModelScope的使用、在ViewModel中启动协程、LiveData与Flow的转换

好,咱们今天聊点实战的。LiveData 和协程,这两个东西在 Android 开发里几乎是标配了。但很多同学把它们割裂开用——LiveData 只管 UI 通知,协程只管异步任务。其实它们能配合得非常好,尤其是在 ViewModel 里。

我个人习惯是:能用协程的地方,尽量不用回调。LiveData 本身不直接支持协程,但 ViewModel 给了我们一个神器——viewModelScope。有了它,协程的生命周期就和 ViewModel 绑定了,再也不用担心泄漏问题。

核心要点:viewModelScope 是 ViewModel 内置的 CoroutineScope,当 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 转成 LiveData
  • asFlow():把 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,方便你保存和回顾:

LiveData 与协程知识体系 ViewModel viewModelScope launch(无返回值) async + await(有返回值) LiveData UI 层观察 Flow ↔ LiveData 转换 asLiveData() asFlow() 生命周期绑定 | 协程安全 | 数据流转换

14.6 总结一下

说白了,LiveData 和协程不是二选一的关系,而是互补的。ViewModel 给了我们 viewModelScope,让我们能安全地启动协程;而 Flow 和 LiveData 之间的转换,让我们既能享受 Flow 强大的操作符,又能保持 LiveData 在 UI 层的简洁性。

我个人建议的实践路径是:

  1. 简单场景:直接用 LiveData + viewModelScope.launch
  2. 复杂数据流:用 Flow 做数据处理,最后 asLiveData() 给 UI
  3. 需要并发:用 async 并行请求,再合并结果
  4. 注意生命周期:理解 asLiveData() 的 timeout 参数,避免数据不刷新

记住一点:工具是死的,思路是活的。LiveData 和 Flow 各有优劣,关键看你的场景适合哪个。别为了用 Flow 而用 Flow,也别死守着 LiveData 不放。灵活搭配,才是高手。

警告:不要在 ViewModel 外部(比如 Activity 或 Fragment 里)使用 viewModelScope。它只能在 ViewModel 内部访问。如果你在 UI 层需要启动协程,请使用 lifecycleScope

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