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 壳」。
20.2 StateFlow:为什么我推荐用它替代 LiveData
LiveData 在 Compose 里能用吗?能。但我不推荐。原因有三:
- LiveData 不支持协程——你想想看,ViewModel 里用 viewModelScope 启动协程,结果数据还得转成 LiveData 发出去,多此一举。
- LiveData 的 observe 在 Compose 里需要额外转换——用
observeAsState()虽然也能用,但不如 StateFlow 原生。 - 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 里写,职责清晰。
.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。
20.5 知识体系总览
下面这张图,帮你把本章的核心逻辑串起来:
20.6 避坑与最佳实践
最后,分享几个我在实战中踩过的坑:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Compose 重组过于频繁 | StateFlow 发射了重复值 | 使用 distinctUntilChanged() 过滤重复 |
| ViewModel 在 Navigation 中共享 | 多个页面共用同一个 ViewModelStoreOwner | 每个 NavBackStackEntry 创建独立的 ViewModel |
| collectAsState 在后台线程更新 UI | StateFlow 在非主线程发射值 | 使用 flowOn(Dispatchers.Main) 确保主线程发射 |
嗯,这一章的内容就到这里。记住一句话:Compose 中的 ViewModel,不是新东西,只是换了个连接方式。 把 StateFlow + collectAsState 用熟,你的 MVVM 架构就能跑得很稳。
公众号:蓝海资料掘金营,微信deep3321