20、Compose与ViewModel:Compose中集成ViewModel、StateFlow、collectAsState、实战:MVVM with Compose

说实话,Jetpack Compose 出来之后,很多兄弟问我同一个问题:「ViewModel 在 Compose 里到底怎么用?」 其实答案很简单——用法没变,但「连接方式」变了。

以前我们用 LiveData + observe,现在换成 StateFlow + collectAsState。我个人习惯把这种组合叫做「响应式铁三角」:ViewModel 管数据、StateFlow 管流动、collectAsState 管重组。

这一章,咱们就把这个铁三角拆开揉碎,再搭一个完整的 MVVM 实战案例。嗯,保证你学完就能用。

20.1 ViewModel 在 Compose 中的「正确打开方式」

先问一个问题:你在 Activity 里怎么拿 ViewModel?

// 传统方式
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)

在 Compose 里,你只需要一行:

@Composable
fun MyScreen() {
    val viewModel: MyViewModel = viewModel()
    // 直接用
}

这个 viewModel() 是 Compose 提供的顶层函数。它会自动帮你做两件事:

  • 找到当前 Activity / Fragment 的 ViewModelStoreOwner
  • 复用已有的 ViewModel 实例(不会重复创建)

我在项目中遇到过一个问题:如果同一个 Composable 被多个地方调用,会不会创建多个 ViewModel? 答案是不会。只要 ViewModelStoreOwner 相同,拿到的就是同一个实例。说白了,Compose 的 viewModel() 就是给 ViewModelProvider 套了一层「Composable 壳」。

💡 小提示: 如果你在 Navigation 中使用,记得把 ViewModelStoreOwner 提升到 NavBackStackEntry 级别,否则多个页面会共享同一个 ViewModel。

20.2 StateFlow:为什么我推荐用它替代 LiveData

LiveData 在 Compose 里能用吗?能。但我不推荐。原因有三:

  1. LiveData 不支持协程——你想想看,ViewModel 里用 viewModelScope 启动协程,结果数据还得转成 LiveData 发出去,多此一举。
  2. LiveData 的 observe 在 Compose 里需要额外转换——用 observeAsState() 虽然也能用,但不如 StateFlow 原生。
  3. StateFlow 天生就是 Kotlin 协程的一部分——冷流变热流,数据不会丢失。

来看一个典型的 ViewModel 写法:

class MyViewModel : ViewModel() {

    // 私有的 MutableStateFlow,对外暴露不可变的 StateFlow
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    fun loadData() {
        viewModelScope.launch {
            _uiState.value = UiState(loading = true)
            try {
                val data = repository.fetchData()
                _uiState.value = UiState(data = data)
            } catch (e: Exception) {
                _uiState.value = UiState(error = e.message)
            }
        }
    }
}

data class UiState(
    val loading: Boolean = false,
    val data: List<String> = emptyList(),
    val error: String? = null
)

这里有个细节:为什么用 asStateFlow() 因为我不希望外部代码能直接修改 _uiState。Compose 里只读,ViewModel 里写,职责清晰。

⚠️ 避坑指南: 我曾经在项目里直接用 MutableStateFlow 暴露给 Compose,结果某个同事在 Composable 里调了 .value = xxx,导致数据流混乱。记住:对外永远暴露不可变的 StateFlow。

20.3 collectAsState:连接 ViewModel 与 Compose 的桥梁

有了 StateFlow,怎么让 Compose 感知数据变化?答案就是 collectAsState()

@Composable
fun MyScreen() {
    val viewModel: MyViewModel = viewModel()
    val uiState by viewModel.uiState.collectAsState()

    // 现在 uiState 就是 Compose 的 State 了
    // 只要 StateFlow 发射新值,这里就会重组
}

你想想看,这背后发生了什么?

  • collectAsState() 启动了一个协程,收集 StateFlow 的数据
  • 每次 StateFlow 发射新值,协程就把值赋给 Compose 的 MutableState
  • Compose 检测到 State 变化,触发重组

说白了,collectAsState() 就是「把 Flow 的异步数据流,变成 Compose 的同步状态」。这个转换非常优雅,不需要手动管理生命周期。

💡 小提示: 如果你需要指定协程作用域,可以用 collectAsStateWithLifecycle(),它来自 lifecycle-runtime-compose 库。这个函数会在 Lifecycle 停止时自动取消收集,避免内存泄漏。

20.4 实战:MVVM with Compose

理论说完了,咱们来搭一个完整的 MVVM 案例。假设我们要做一个「用户列表」页面,从网络加载数据,显示加载中、成功、失败三种状态。

第一步:定义 UiState

sealed class UserListUiState {
    object Loading : UserListUiState()
    data class Success(val users: List<User>) : UserListUiState()
    data class Error(val message: String) : UserListUiState()
}

用 sealed class 的好处是:状态是互斥的。不会出现 loading=true 同时 data 也有值的情况。我在项目中吃过这个亏,后来统一改用 sealed class,代码清晰多了。

第二步:ViewModel

class UserListViewModel(
    private val repository: UserRepository = UserRepository()
) : ViewModel() {

    private val _uiState = MutableStateFlow<UserListUiState>(UserListUiState.Loading)
    val uiState: StateFlow<UserListUiState> = _uiState.asStateFlow()

    init {
        loadUsers()
    }

    fun loadUsers() {
        viewModelScope.launch {
            _uiState.value = UserListUiState.Loading
            try {
                val users = repository.getUsers()
                _uiState.value = UserListUiState.Success(users)
            } catch (e: Exception) {
                _uiState.value = UserListUiState.Error(e.message ?: "未知错误")
            }
        }
    }
}

第三步:Composable 层

@Composable
fun UserListScreen() {
    val viewModel: UserListViewModel = viewModel()
    val uiState by viewModel.uiState.collectAsState()

    when (val state = uiState) {
        is UserListUiState.Loading -> {
            CircularProgressIndicator()
        }
        is UserListUiState.Success -> {
            LazyColumn {
                items(state.users) { user ->
                    UserItem(user)
                }
            }
        }
        is UserListUiState.Error -> {
            Column {
                Text("加载失败:${state.message}")
                Button(onClick = { viewModel.loadUsers() }) {
                    Text("重试")
                }
            }
        }
    }
}

你看,整个流程非常清晰:

  • ViewModel 负责业务逻辑和数据加载
  • StateFlow 负责数据流动
  • Compose 只负责渲染

这就是 MVVM 在 Compose 中的最佳实践。我习惯把这种模式叫做「单向数据流」——数据从 ViewModel 流向 UI,UI 通过事件反向通知 ViewModel。

🎯 核心要点: Compose 中的 MVVM,本质上就是「StateFlow 驱动重组」。你不需要手动调用任何刷新方法,数据变了,界面自动变。

20.5 知识体系总览

下面这张图,帮你把本章的核心逻辑串起来:

Compose + ViewModel + StateFlow 架构图 ViewModel 层 viewModelScope.launch MutableStateFlow asStateFlow() StateFlow 层 冷流 → 热流 数据不丢失 协程原生 Compose 层 collectAsState() State → 重组 UI 渲染 发射 收集 数据流向: 1. ViewModel 在 viewModelScope 中发起网络请求 2. 结果通过 MutableStateFlow.value 发射 3. Compose 通过 collectAsState() 收集,触发重组 4. UI 事件(如点击重试)回调 ViewModel 的方法 事件回调(点击重试 → loadUsers())

20.6 避坑与最佳实践

最后,分享几个我在实战中踩过的坑:

问题 原因 解决方案
Compose 重组过于频繁 StateFlow 发射了重复值 使用 distinctUntilChanged() 过滤重复
ViewModel 在 Navigation 中共享 多个页面共用同一个 ViewModelStoreOwner 每个 NavBackStackEntry 创建独立的 ViewModel
collectAsState 在后台线程更新 UI StateFlow 在非主线程发射值 使用 flowOn(Dispatchers.Main) 确保主线程发射

嗯,这一章的内容就到这里。记住一句话:Compose 中的 ViewModel,不是新东西,只是换了个连接方式。 把 StateFlow + collectAsState 用熟,你的 MVVM 架构就能跑得很稳。


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