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 就是来解决这个问题的。

核心结论: ViewModel 的生命周期独立于 Composable,它跟 Activity/Fragment 共存亡。配置变更时,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 而不是 LiveDataFlow。为什么?因为在 Compose 里,State 是原生响应式对象,Compose 能直接订阅它的变化,不需要额外转换。我个人习惯,只要 ViewModel 里的状态是给 Compose 用的,一律用 State

小技巧: 如果你非要用 LiveData,可以用 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 包一下),会更安全。

我曾经踩过的坑: 在 Factory 里直接传 Activity 或 Context。ViewModel 不应该持有 Activity 引用,否则可能内存泄漏。如果非要 Context,用 AndroidViewModel

10.5 ViewModel 的作用域:谁拥有它?

viewModel() 默认作用域是最近的 ActivityFragment。但如果你在 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 中的核心逻辑:

Compose 中 ViewModel 集成核心逻辑 Composable 层 viewModel() 函数 自动获取/创建 订阅 State 变化 ViewModel 层 mutableStateOf / State LiveData → observeAsState() 业务逻辑处理 生命周期层 Activity / Fragment NavBackStackEntry 配置变更不销毁 ViewModel 通过 viewModel() 与 Composable 绑定 生命周期由最近的 Activity / NavBackStackEntry 管理 状态变化自动驱动 UI 重组 ⚠️ 注意:不要在 ViewModel 中持有 Composable 引用 ViewModel 只负责状态和逻辑,不负责 UI 对象

10.7 实战建议:ViewModel 该放什么?

很多新手会问:ViewModel 里到底该写什么?我总结了几条原则:

  • 放状态: 所有 UI 需要的数据,比如列表、输入框内容、加载状态。
  • 放逻辑: 点击事件处理、网络请求、数据转换。但不要放 UI 操作(比如弹 Toast 或导航)。
  • 不放 Context: 除非用 AndroidViewModel,否则别碰 Context。
  • 不放 Composable 对象: ViewModel 里不要出现 @Composable 函数或 Composable 引用。

说白了,ViewModel 就是你的「数据管家」。它把状态管好,Compose 负责把状态画出来。各司其职,代码才清爽。

我的习惯: 每个 ViewModel 只对应一个页面或一个功能模块。不要搞「万能 ViewModel」,那样维护起来会想哭。

10.8 避坑指南

最后分享几个我踩过的坑:

  • 坑一: 在 Composable 里用 remember 包裹 ViewModel。千万别!viewModel() 本身就有缓存机制,不需要你再包一层。
  • 坑二: 在 ViewModel 里用 mutableStateOf 但忘记用 by 委托。如果你写 val count = viewModel.count.value,Compose 可能不会正确重组。记得用 by 或者 .value 在 Composable 里读取。
  • 坑三: 多个 Composable 共享同一个 ViewModel 实例时,状态同步问题。我建议用 hiltViewModel() 或手动指定作用域,避免意外共享。

嗯,大概就这些。ViewModel 集成其实不难,但细节决定体验。你按我说的做,转屏丢数据这种低级 bug 基本不会再出现。


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