14、协程与ViewModel:viewModelScope的最佳实践、ViewModel中管理协程任务、ViewModel与Flow的结合、在ViewModel中处理UI状态

说实话,ViewModel 和协程的搭配,是我在项目中用得最频繁的组合之一。很多开发者刚接触时,总觉得「不就是 launch 一下嘛」,但真正上线后,各种内存泄漏、任务丢失、状态不一致的问题就冒出来了。今天我就把这几年的实战经验掰开揉碎,跟你聊聊怎么用好它们。

viewModelScope:为什么它是「官方指定」的协程作用域?

先问个问题:你在 ViewModel 里启动协程时,用的是 GlobalScope 还是 lifecycleScope?

我个人习惯只用 viewModelScope。原因很简单——它会在 ViewModel 被清除时自动取消所有协程。你想想看,如果用了 GlobalScope,Activity 都销毁了,协程还在后台跑,轻则浪费资源,重则更新已销毁的 UI 引发崩溃。

核心要点:viewModelScope 是 ViewModel 内置的 CoroutineScope,它的生命周期与 ViewModel 绑定。ViewModel 被 clear() 时,scope 会自动 cancel。

来看一段我在项目中常用的写法:

class UserViewModel : ViewModel() {
    private val userRepo = UserRepository()

    fun loadUser(userId: String) {
        viewModelScope.launch {
            val user = userRepo.fetchUser(userId)
            // 更新 UI
        }
    }
}

嗯,这里要注意:viewModelScope 默认使用 Dispatchers.Main。为什么?因为 ViewModel 的数据最终要交给 UI 层,主线程直接更新最安全。如果你需要做耗时操作,用 withContext(Dispatchers.IO) 切一下就好。

我的习惯:在 ViewModel 中,我几乎从不手动创建 CoroutineScope。viewModelScope 已经够用了,除非你有非常特殊的生命周期需求。

ViewModel 中管理协程任务:别让任务「失控」

我在项目中遇到过一个问题:用户快速点击「刷新」按钮,结果同时启动了 5 个网络请求,页面数据乱跳。这就是典型的协程任务管理不当。

怎么解决?我总结了三个原则:

  • 取消旧任务:用 Job 来控制,启动新任务前取消旧的。
  • 限制并发:用 Mutex 或 Semaphore 控制同一时间只能有一个任务执行。
  • 任务分组:用 SupervisorJob 让子任务失败不影响其他任务。

举个例子,取消旧任务的写法:

class SearchViewModel : ViewModel() {
    private var searchJob: Job? = null

    fun search(query: String) {
        searchJob?.cancel() // 取消上一次搜索
        searchJob = viewModelScope.launch {
            delay(300) // 防抖
            val results = repository.search(query)
            _searchResults.value = results
        }
    }
}

说白了,这就是「谁先来谁先走,后来的覆盖前面的」。用户只关心最后一次搜索的结果,之前的请求完全可以丢掉。

我曾经踩过的坑:用 cancel() 取消协程时,如果协程内部没有做 isActive 检查,取消可能不会立即生效。记得在循环或耗时操作中加 ensureActive()yield()

ViewModel 与 Flow 的结合:数据流的「黄金搭档」

Flow 和 ViewModel 的配合,说白了就是「数据生产者在后台,消费者在前台」。我常用的模式是:Repository 返回 Flow,ViewModel 用 stateIn 转为 StateFlow,UI 层直接收集。

来看一个完整的例子:

class DashboardViewModel : ViewModel() {
    private val repo = DashboardRepository()

    // 方式1:直接暴露 Flow
    val realtimeData: StateFlow<DashboardState> = repo.observeRealtimeData()
        .map { DashboardState.Success(it) }
        .catch { emit(DashboardState.Error(it.message ?: "未知错误")) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = DashboardState.Loading
        )
}

这里有个细节:SharingStarted.WhileSubscribed(5000)。为什么是 5000 毫秒?因为当 Activity 重建时(比如旋转屏幕),会有短暂的订阅中断。设置 5000ms 的缓冲,可以避免数据流频繁重启。我个人习惯用 5000,你也可以根据场景调整。

最佳实践:ViewModel 中尽量用 StateFlow 暴露状态,不要用 LiveData。StateFlow 天然支持协程,配合 stateInflowWithLifecycle 使用,生命周期管理更优雅。

在 ViewModel 中处理 UI 状态:告别「面条式代码」

UI 状态管理,说白了就是「页面该显示什么」。我见过太多人用多个 LiveData 分别控制加载、错误、数据,结果状态组合起来乱成一锅粥。

我的做法是:用一个密封类(sealed class)定义所有状态。

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>()
}

class ProfileViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState<Profile>>(UiState.Loading)
    val uiState: StateFlow<UiState<Profile>> = _uiState.asStateFlow()

    fun loadProfile(userId: String) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val profile = repo.getProfile(userId)
                _uiState.value = UiState.Success(profile)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "加载失败")
            }
        }
    }
}

UI 层只需要收集 uiState,然后 when 匹配三种状态即可。代码清晰,逻辑集中,维护起来特别舒服。

避坑指南:我曾经在状态类里直接放「错误码 + 错误消息」两个字段,结果 UI 层要判断各种组合。后来改成密封类,每个状态只包含必要数据,再也没出过状态不一致的问题。

知识体系总览

下面这张图是我自己总结的 ViewModel + 协程的核心结构,你可以对照着理解:

ViewModel + 协程核心架构 ViewModel viewModelScope 管理生命周期 协程任务管理 Job取消 / 并发控制 Flow 数据流 stateIn / 冷流转热流 UI 状态管理 密封类 UiState:Loading / Success / Error 关键原则 • viewModelScope 自动管理生命周期,避免内存泄漏 • 用 Job 或 Mutex 控制协程任务,防止重复请求 • StateFlow + 密封类,让 UI 状态清晰可预测

总结一下

ViewModel 和协程的配合,核心就三件事:

  • 作用域:用 viewModelScope,别自己瞎搞。
  • 任务管理:该取消就取消,该防抖就防抖。
  • 状态管理:用密封类 + StateFlow,别用一堆 LiveData 拼凑。

做到这三点,你的 ViewModel 代码会变得非常干净,维护起来也轻松。我在好几个项目里都是这么做的,从来没出过协程相关的线上问题。


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