【Compose 系列】第 3 篇:状态管理入门,让界面「动」起来

定位:入门 / 核心心智 适合:会用 Compose 写静态界面,但不知道数据变化后界面为何能自动更新


一、从「静态界面」到「会动的界面」

上一篇我们知道,Composable 函数会被反复调用。但反复调用一个「每次都一样」的函数,界面永远是静态的。那界面什么时候会变?答案是:当函数里读取的状态(state)变了

打个比方:Compose 像一个「自动出餐机器人」,你告诉它配方「汉堡 = 面包 + 肉饼 + 生菜」。只要「肉饼」这个原料没变,它就一直做出同样的汉堡。一旦你把「肉饼」换成「鸡排」,它自动重做一遍,出来的就是鸡肉堡。

这里的「肉饼」就是状态,机器人自动重做就是重组。


二、第一个会动的状态:计数器

kotlin 复制代码
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }  // ① 声明状态
    Button(onClick = { count++ }) {              // ② 修改状态
        Text("点击了 $count 次")
    }
}

三个关键点:

  1. mutableStateOf(0):创建一个「可观察的状态容器」,里面装的是 0。当它的值变化时,Compose 会收到通知。
  2. remember { } :让这个容器在重组时存活。上一篇说过,函数每次重组都会重新执行,如果不 remembermutableStateOf(0) 每次都会被重建,状态就归零了。
  3. by 委托 :让你直接写 count++,而不是 count.value++。读起来像普通变量,但底层是可观察的。

整个流程:点击 → count++ → 状态容器通知「我变了」→ Compose 找到读了 countCounter → 重新执行它 → 按钮文字更新。


三、为什么必须 remember

这是小白最容易踩的坑。看这个「反例」:

kotlin 复制代码
@Composable
fun BrokenCounter() {
    var count = 0                      // 没有 remember,也没有 mutableStateOf
    Button(onClick = { count++ }) {
        Text("点击了 $count 次")
    }
}

你点按钮,count++ 执行了,但界面纹丝不动。原因有两层:

  1. count 不是可观察状态,改它 Compose 根本不知道;
  2. 即使你加了 mutableStateOf 但忘了 remember,每次重组都会重新创建状态,永远归零。

所以记忆口诀:「要变」用 mutableStateOf,「要活」用 remember 两个缺一不可。


四、状态提升(State Hoisting):让组件可复用

现在有个问题:如果状态写在 Counter 内部,别人就看不到、控制不了它。比如你想在页面顶部再显示一次计数,或者在外部重置它,就做不到。

解决办法叫状态提升:把状态从组件内部「提升」到父组件,通过参数传下来。

kotlin 复制代码
// 无状态组件:只负责显示和回调,不持有状态
@Composable
fun Counter(
    count: Int,
    onIncrement: () -> Unit,
) {
    Button(onClick = onIncrement) {
        Text("点击了 $count 次")
    }
}

// 父组件:持有状态
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }
    Column {
        Counter(count = count, onIncrement = { count++ })  // 状态向下传
        Text("总次数:$count")                              // 状态可以复用
        Button(onClick = { count = 0 }) { Text("重置") }     // 外部也能改
    }
}

状态提升带来的好处:

  • 单一数据源:状态只有一份,放在父组件,多个子组件读取同一份,不会各管各的。
  • 可复用Counter 变成纯展示组件,换到任何地方都能用。
  • 可测试:无状态组件输入输出明确,很好写测试。

这其实是 单向数据流(UDF) 的雏形:状态向下流动,事件向上流动。第 5 篇会把它推广到 ViewModel 架构。


五、资深开发者的醍醐灌顶时刻

  1. 状态 = 单一事实来源(Single Source of Truth)。同样的 UI 状态,只能有一个地方产生它。状态提升是手段,单一数据源是目标。这直接对应架构里的 UDF。
  2. 「智能跳过」依赖状态读取的位置。Compose 只重组「真正读取了变化的那个 state」的组件。所以状态提升 + 组件拆分,天然帮你缩小重组范围。这是第 10 篇性能优化的地基。
  3. remember 的「key」remember(key1) { ... } 可以在 key 变化时重新计算。比如 remember(userId) { loadUser(userId) },用户切换时自动刷新。理解 key 是理解后续 LaunchedEffect(userId) 的钥匙。
  4. 可观察状态 ≠ 可变普通变量mutableStateOf 是一个带订阅机制的容器,不是语法糖。理解「订阅-通知」模型,是理解 Compose 运行时的关键。

六、常见坑

  • 把状态写在 Composable 外:类成员变量、单例......Compose 不知道它们变化,界面不会更新。
  • remembermutableStateOfremember { 0 } 记住了 0,但改了不会触发重组。
  • mutableStateOfremember:每次重组状态归零。
  • 不必要的状态 :能用 val 算出来的就别存状态。比如 val isEven = count % 2 == 0 直接算,别 remember { mutableStateOf(count % 2 == 0) }。冗余状态是 bug 温床。
  • 状态放错层级:一个状态只被一个组件用,就别提升到全局。提升到「刚好够用的最上层」即可。

七、动手题

实现一个「水温指示器」:一个滑块(Slider)调节温度 0--100,界面显示温度值,并根据温度显示「冷水 / 温水 / 热水」。要求:状态提升,滑块组件和显示组件分离。


八、学习资料

(下一篇预告:布局与 Modifier,写界面不走弯路。)

相关推荐
alexhilton1 天前
规范驱动开发:让AI生成符合预期的KMP代码
android·kotlin·android jetpack
一个用户名i3 天前
【Compose 系列】第 1 篇:认识 Compose,为什么要学它
android·android jetpack
我命由我123455 天前
Android 开发问题:unexpected element <service> found in <manifest>
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
我命由我123455 天前
Android 开发问题:TopAppBar 和 topAppBarColors API is experimental...
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
alexhilton8 天前
千万别误用Android Skills
android·kotlin·android jetpack
XiaoLeisj10 天前
Kotlin Flow 常用操作符:数据变换 map、filter、onEach,时间控制 debounce、sample,终端聚合 reduce、fold
android·kotlin·android jetpack·协程·响应式编程·flow
XiaoLeisj11 天前
Kotlin Flow:冷流收集、并行订阅、emit 推送、delay 延迟、collect 全量收集与 collectLatest 的实时数据处理
android·kotlin·android jetpack·协程·flow
我命由我1234512 天前
Jetpack Compose - @Preview 注解
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
学习使我健康15 天前
一个基于 **Jetpack Compose + Material 3** 的 Android 入门示例项目,演示 Compose 声明式 UI 的核心用法
ui·kotlin·android jetpack