8、状态管理(上):remember与mutableStateOf、State与MutableState、状态提升、ViewModel集成

状态管理,说白了就是解决「数据变了,UI怎么跟着变」这个问题。

我刚接触 Compose 时,第一反应是:这不就是 setState 吗?后来发现,Compose 的状态管理比我想象的深得多。它不只是一个 API,而是一套完整的响应式编程模型。

这一章,我们先啃下状态管理的核心基础。后面还有下篇,会讲更高级的玩法。

8.1 从 mutableStateOf 说起

在 Compose 里,最简单的状态声明方式就是 mutableStateOf

@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }

    Button(onClick = { count.value++ }) {
        Text("点击了 ${count.value} 次")
    }
}

这段代码里,mutableStateOf(0) 创建了一个可观察的状态容器。remember 保证重组时不会重新创建。

我刚开始写的时候犯过一个低级错误:忘了加 remember。结果每次重组都重置为 0,按钮怎么点都不变。嗯,这个坑我替你们踩过了。

核心要点:

  • mutableStateOf 返回的是 MutableState<T> 类型
  • 读取用 .value,修改也是 .value = xxx
  • 必须配合 remember 使用,否则状态会丢失

8.2 State 与 MutableState 的区别

这两个接口,很多人搞混过。我简单说一下:

接口 可读 可写 典型来源
State<T> 派生状态、ViewModel 暴露
MutableState<T> 局部状态、表单输入

State 是只读的,MutableState 继承自 State,多了写的能力。

你想想看,为什么要有这个区分?

因为在实际项目中,我们希望「数据源」只有一个。ViewModel 里持有 MutableState,但对外只暴露 State。这样 UI 层只能读,不能乱改。我在项目里见过有人直接把 MutableState 传到各个 Composable 里,结果到处都在改数据,调试起来简直噩梦。

我的习惯:ViewModel 内部用 MutableState,对外暴露用 State。UI 层只读,不改。

8.3 remember 的底层逻辑

remember 的作用是「记住」一个值,跨重组保留。

@Composable
fun RememberDemo() {
    val random = remember { Random.nextInt() }
    val random2 = Random.nextInt()

    Text("remember: $random")   // 重组时不变
    Text("no remember: $random2") // 重组时重新计算
}

remember 不是万能的。它只在当前 Composable 的生命周期内有效。如果 Composable 被移出组合树,remember 的值就丢了。

我曾经遇到一个场景:用户切换 Tab 时,之前输入的内容全没了。原因就是 Tab 切换导致 Composable 被销毁重建,remember 自然保不住数据。这时候就需要 ViewModel 出马了。

注意:remember 不跨配置变更(如屏幕旋转)。如果需要持久化,请用 ViewModel 或 SavedStateHandle。

8.4 状态提升:把状态往上提

状态提升,是 Compose 里一个非常重要的设计模式。说白了就是:把状态放到父组件里,子组件只负责触发事件

@Composable
fun Parent() {
    var text by remember { mutableStateOf("") }

    Child(
        value = text,
        onValueChange = { text = it }
    )
}

@Composable
fun Child(value: String, onValueChange: (String) -> Unit) {
    TextField(value = value, onValueChange = onValueChange)
}

这样做的好处很明显:

  • 单一数据源:状态只在一个地方维护
  • 可复用Child 不依赖任何外部状态,传入什么就显示什么
  • 可测试:直接传参数就能测试子组件

我个人习惯:如果一个 Composable 内部状态超过两个,或者状态需要被兄弟组件共享,就立刻做状态提升。

8.5 ViewModel 集成:真正的状态管理

前面说的 remember 和状态提升,适合简单场景。但真实项目里,状态往往需要跨页面、跨配置变更存活。这时候就要上 ViewModel 了。

class MyViewModel : ViewModel() {
    private val _count = mutableStateOf(0)
    val count: State<Int> = _count

    fun increment() {
        _count.value++
    }
}

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

    Button(onClick = { viewModel.increment() }) {
        Text("点击了 $count 次")
    }
}

注意这里:viewModel() 是 Compose 提供的函数,它会自动管理 ViewModel 的生命周期。屏幕旋转时,ViewModel 不会销毁,状态自然保留。

ViewModel 的优势:

  • 跨配置变更存活
  • 业务逻辑与 UI 分离
  • 天然支持协程作用域
  • 方便单元测试

我在项目中遇到过一个问题:ViewModel 里用 mutableStateOf 时,忘了用 by 委托,结果每次都要写 .value。虽然不影响功能,但代码看起来啰嗦。后来我统一用 by 委托,清爽多了。

8.6 本章知识体系

下面这张图,把本章的核心概念串起来了:

状态管理(上)核心知识体系 Compose 状态管理 mutableStateOf State / MutableState 状态提升 ViewModel 集成 remember 配合 响应式更新 只读 vs 可写 数据封装 单一数据源 事件回调 生命周期感知 业务逻辑分离 核心原则:状态向下,事件向上

8.7 避坑指南

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

  • 不要在 Composable 里直接创建 ViewModel:用 viewModel() 函数,不要自己 new。
  • 不要用 var 声明状态var count by remember { mutableStateOf(0) } 才是正确写法。
  • 状态提升别过度:如果状态只在当前组件内使用,没必要提到父组件去。
  • ViewModel 里别持有 Composable 引用:ViewModel 生命周期比 Composable 长,容易内存泄漏。

嗯,这一章的内容就到这里。状态管理是 Compose 的核心,理解了这些基础,后面学高级特性会轻松很多。


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