10、ViewModel 集成:在 Compose 中使用 ViewModel、viewModel() 函数
说实话,ViewModel 这东西,在传统 View 体系里就已经是「状态救星」了。到了 Compose 里,它的地位不但没降,反而更核心了。你想想看,Compose 是声明式 UI,界面随状态自动刷新。那状态放哪?总不能全塞在 Composable 函数里吧?那样一重建就全丢了。
所以,ViewModel 就是那个「状态保险箱」。它跟 Activity/Fragment 的生命周期走,配置变更(比如转屏)不会丢数据。在 Compose 里用起来,比传统方式还顺手。我个人习惯,几乎每个带交互的页面都会配一个 ViewModel。
10.1 为什么 Compose 需要 ViewModel?
先看一个反面教材。假设你写了个计数器:
@Composable
fun BadCounter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("点击: $count")
}
}
这段代码有什么问题?嗯,转屏试试。count 直接归零。因为 Activity 重建,Composable 树也跟着重建,remember 虽然能撑住重组,但扛不住配置变更。
我在项目中遇到过类似的情况——一个表单页面,用户填了十几项,转屏后全没了。那用户能忍?肯定不能。所以 ViewModel 就是来解决这个问题的。
10.2 添加依赖
在 Compose 里用 ViewModel,首先得加依赖。别嫌麻烦,这一步跳不过去。
// build.gradle.kts (Module)
dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
}
这个库提供了 viewModel() 函数,是 Compose 集成 ViewModel 的关键。版本号建议用最新的稳定版,我写文章时是 2.6.2。
10.3 viewModel() 函数:最简单的集成方式
在 Composable 里获取 ViewModel,只需要一行:
@Composable
fun MyScreen() {
val viewModel: MyViewModel = viewModel()
// 使用 viewModel 的状态
}
就这么简单。没有 ViewModelProvider,没有 Factory(除非有构造参数)。viewModel() 会自动帮你找到或创建 ViewModel,作用域默认是最近的 Activity 或 Fragment。
来看一个完整的计数器例子:
class CounterViewModel : ViewModel() {
private val _count = mutableStateOf(0)
val count: State<Int> = _count
fun increment() {
_count.value++
}
}
@Composable
fun CounterScreen() {
val viewModel: CounterViewModel = viewModel()
val count by viewModel.count
Column {
Text("计数: $count")
Button(onClick = { viewModel.increment() }) {
Text("加一")
}
}
}
注意这里我用的是 mutableStateOf 而不是 LiveData 或 Flow。为什么?因为在 Compose 里,State 是原生响应式对象,Compose 能直接订阅它的变化,不需要额外转换。我个人习惯,只要 ViewModel 里的状态是给 Compose 用的,一律用 State。
observeAsState() 转换。但多一层转换就多一层开销,能省则省。
10.4 带参数的 ViewModel:使用 ViewModelProvider.Factory
有时候 ViewModel 需要参数,比如用户 ID 或仓库依赖。这时候 viewModel() 就不能直接用了,得传一个 Factory。
class UserViewModel(
private val userId: String,
private val repo: UserRepository
) : ViewModel() {
// ...
}
@Composable
fun UserScreen(userId: String) {
val factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return UserViewModel(userId, UserRepository()) as T
}
}
val viewModel: UserViewModel = viewModel(factory = factory)
// ...
}
嗯,这里要小心。每次重组时,factory 对象都会重新创建。但 ViewModel 只会创建一次,所以 Factory 本身的重建不影响。不过,如果你把 factory 放在 Composable 外面(比如用 remember 包一下),会更安全。
AndroidViewModel。
10.5 ViewModel 的作用域:谁拥有它?
viewModel() 默认作用域是最近的 Activity 或 Fragment。但如果你在 Navigation Compose 里用,作用域会变成 NavBackStackEntry。这意味着,每个导航目的地都有自己的 ViewModel 实例。
看个例子:
NavHost(navController, startDestination = "home") {
composable("home") {
val vm: HomeViewModel = viewModel() // 属于 home 这个目的地
}
composable("detail/{id}") {
val vm: DetailViewModel = viewModel() // 属于 detail 这个目的地
}
}
两个 ViewModel 互不干扰。如果你想让多个目的地共享同一个 ViewModel,可以用 viewModel() 指定作用域为父级 NavGraph 或 Activity。但说实话,我很少这么做——共享状态容易让数据流变乱。
10.6 知识体系图
下面这张图总结了 ViewModel 在 Compose 中的核心逻辑:
10.7 实战建议:ViewModel 该放什么?
很多新手会问:ViewModel 里到底该写什么?我总结了几条原则:
- 放状态: 所有 UI 需要的数据,比如列表、输入框内容、加载状态。
- 放逻辑: 点击事件处理、网络请求、数据转换。但不要放 UI 操作(比如弹 Toast 或导航)。
- 不放 Context: 除非用 AndroidViewModel,否则别碰 Context。
- 不放 Composable 对象: ViewModel 里不要出现
@Composable函数或 Composable 引用。
说白了,ViewModel 就是你的「数据管家」。它把状态管好,Compose 负责把状态画出来。各司其职,代码才清爽。
10.8 避坑指南
最后分享几个我踩过的坑:
- 坑一: 在 Composable 里用
remember包裹 ViewModel。千万别!viewModel()本身就有缓存机制,不需要你再包一层。 - 坑二: 在 ViewModel 里用
mutableStateOf但忘记用by委托。如果你写val count = viewModel.count.value,Compose 可能不会正确重组。记得用by或者.value在 Composable 里读取。 - 坑三: 多个 Composable 共享同一个 ViewModel 实例时,状态同步问题。我建议用
hiltViewModel()或手动指定作用域,避免意外共享。
嗯,大概就这些。ViewModel 集成其实不难,但细节决定体验。你按我说的做,转屏丢数据这种低级 bug 基本不会再出现。
公众号:蓝海资料掘金营,微信deep3321