【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 { } :让这个容器在重组时存活。上一篇说过,函数每次重组都会重新执行,如果不 remember,mutableStateOf(0) 每次都会被重建,状态就归零了。
  3. by 委托 :让你直接写 count++,而不是 count.value++。读起来像普通变量,但底层是可观察的。

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


三、为什么必须 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 不知道它们变化,界面不会更新。
  • 只 remember 不 mutableStateOf :remember { 0 } 记住了 0,但改了不会触发重组。
  • 只 mutableStateOf 不 remember:每次重组状态归零。
  • 不必要的状态 :能用 val 算出来的就别存状态。比如 val isEven = count % 2 == 0 直接算,别 remember { mutableStateOf(count % 2 == 0) }。冗余状态是 bug 温床。
  • 状态放错层级:一个状态只被一个组件用,就别提升到全局。提升到「刚好够用的最上层」即可。

七、动手题

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


八、学习资料

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

相关推荐
天空之城--1 天前
Android一周动态:Android 18首次官宣、Compose Material3 1.4转正(5趋势+5资讯)
android·人工智能·flutter·架构·android jetpack
传奇开心果编程2 天前
【现代声明式UI学与练】第1课 从命令式UI到声明式UI
学习·flutter·ui·swiftui·react·android jetpack
传奇开心果编程5 天前
【用案例学Material 3 Expressive】第1课:从一封邮件开始,感受安卓设计新语言的表现力
android·学习·ui·kotlin·android jetpack
传奇开心果编程5 天前
【Jetpack Compose进阶学与练】第14课:系列收尾复习总结;Compose项目常见坑点汇总;学习路线与后续学习方向
android·学习·ui·kotlin·android jetpack
hai_android6 天前
子 View 被压小时,如何向父容器"告状"?——MEASURED_STATE_TOO_SMALL 向上传递机制
android·kotlin·android jetpack
alexhilton6 天前
生命周期感知的多模块启动模式
android·kotlin·android jetpack
hai_android7 天前
RecyclerView 缓存机制详解
android·kotlin·android jetpack
hai_android7 天前
深入理解 Java 的四种引用:强引用、软引用、弱引用、虚引用
android·kotlin·android jetpack
zbmwa7 天前
jetpack compose 副作用 SideEffect
android jetpack
alexhilton13 天前
藏在设备上的秘密,终究藏不住
android·kotlin·android jetpack