定位:入门 / 核心心智 适合:会用 Compose 写静态界面,但不知道数据变化后界面为何能自动更新
一、从「静态界面」到「会动的界面」
上一篇我们知道,Composable 函数会被反复调用。但反复调用一个「每次都一样」的函数,界面永远是静态的。那界面什么时候会变?答案是:当函数里读取的状态(state)变了。
打个比方:Compose 像一个「自动出餐机器人」,你告诉它配方「汉堡 = 面包 + 肉饼 + 生菜」。只要「肉饼」这个原料没变,它就一直做出同样的汉堡。一旦你把「肉饼」换成「鸡排」,它自动重做一遍,出来的就是鸡肉堡。
这里的「肉饼」就是状态,机器人自动重做就是重组。
二、第一个会动的状态:计数器
kotlin
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) } // ① 声明状态
Button(onClick = { count++ }) { // ② 修改状态
Text("点击了 $count 次")
}
}
三个关键点:
mutableStateOf(0):创建一个「可观察的状态容器」,里面装的是 0。当它的值变化时,Compose 会收到通知。remember { }:让这个容器在重组时存活。上一篇说过,函数每次重组都会重新执行,如果不remember,mutableStateOf(0)每次都会被重建,状态就归零了。by委托 :让你直接写count++,而不是count.value++。读起来像普通变量,但底层是可观察的。
整个流程:点击 → count++ → 状态容器通知「我变了」→ Compose 找到读了 count 的 Counter → 重新执行它 → 按钮文字更新。
三、为什么必须 remember
这是小白最容易踩的坑。看这个「反例」:
kotlin
@Composable
fun BrokenCounter() {
var count = 0 // 没有 remember,也没有 mutableStateOf
Button(onClick = { count++ }) {
Text("点击了 $count 次")
}
}
你点按钮,count++ 执行了,但界面纹丝不动。原因有两层:
count不是可观察状态,改它 Compose 根本不知道;- 即使你加了
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 架构。
五、资深开发者的醍醐灌顶时刻
- 状态 = 单一事实来源(Single Source of Truth)。同样的 UI 状态,只能有一个地方产生它。状态提升是手段,单一数据源是目标。这直接对应架构里的 UDF。
- 「智能跳过」依赖状态读取的位置。Compose 只重组「真正读取了变化的那个 state」的组件。所以状态提升 + 组件拆分,天然帮你缩小重组范围。这是第 10 篇性能优化的地基。
remember的「key」 :remember(key1) { ... }可以在 key 变化时重新计算。比如remember(userId) { loadUser(userId) },用户切换时自动刷新。理解 key 是理解后续LaunchedEffect(userId)的钥匙。- 可观察状态 ≠ 可变普通变量 。
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,界面显示温度值,并根据温度显示「冷水 / 温水 / 热水」。要求:状态提升,滑块组件和显示组件分离。
八、学习资料
- 官方《State and Jetpack Compose》:developer.android.com/develop/ui/...
- 官方《State hoisting》:developer.android.com/develop/ui/...
- 官方《Architecture in Compose》(UDF 思想):developer.android.com/develop/ui/...
(下一篇预告:布局与 Modifier,写界面不走弯路。)