状态是什么

Compose 是声明式 UI:界面是状态的函数,状态变则界面变。这里的「状态」指任何随时间变化、能驱动 UI 的值——输入框文字、列表数据、开关选中、加载状态都算。

声明式的核心承诺:只改数据,界面自动跟着改。

因此状态管理的本质问题只有两个:状态放哪、状态怎么变。放得近则简单直接,放得远则共享方便,怎么变则决定谁能触发重组。

remember 与状态持有

remember 让值在重组之间存活。普通变量每次重组都重建,remember 包住的值只建一次。remember 内部用 mutableStateOf 包装,Compose 才能感知变化。

@Composable
fun Counter() {
  var count by remember { mutableStateOf(0) }
  Button(onClick = { count++ }) {
    Text("点击 $count 次")
  }
}

by 委托写法最简洁,count 读写都自动命中 State。注意 remember 只在组合内存活,配置变更(旋转屏幕)后也会重置;要跨配置存活,用 rememberSaveable。

mutableStateOf 与状态读写

mutableStateOf 创建可观察状态,读写它就会触发依赖它的组件重组。状态粒度要适中:整页一个大状态和每字段一个状态,性能天差地别。

@Composable
fun LoginForm() {
  var username by remember { mutableStateOf("") }
  var password by remember { mutableStateOf("") }
  var loading by remember { mutableStateOf(false) }

  Column {
    OutlinedTextField(value = username, onValueChange = { username = it })
    OutlinedTextField(value = password, onValueChange = { password = it })
    Button(enabled = !loading, onClick = { loading = true }) {
      Text(if (loading) "提交中" else "登录")
    }
  }
}

细粒度状态让每次输入只重组输入框本身,而不是整屏。能用基本类型就别装箱,能拆开就别捆在一起。

状态提升与重组治理

状态提升:把状态挪到共同父级,子组件通过参数收状态、通过回调改状态。无状态组件可复用、可测试,这是 Compose 组件设计的黄金法则。

@Composable
fun TodoItem(
  text: String,
  done: Boolean,
  onToggle: () -> Unit
) {
  Row {
    Checkbox(checked = done, onCheckedChange = { onToggle() })
    Text(text)
  }
}

@Composable
fun TodoScreen() {
  var items by remember { mutableStateOf(listOf(Todo("学习状态", false))) }
  TodoItem(text = items[0].text, done = items[0].done) {
    items = items.map { it.copy(done = !it.done) }
  }
}

重组治理还有两招:lambda 参数抽成 remember 避免每次重建、稳定类型用 immutable 标注。列表用 LazyColumn,别在 Column 里循环塞上千行。

关于重组范围,很多初学者容易误解:Compose 的重组是智能分片的,状态变化只会让读取了该状态的代码块重新执行,而不是整个组件树全部重建。利用这个特性,可以把耗时的计算放到 remember 里,把不必要的读取从重组频繁的节点移出去。另一个常见问题是派生状态的处理,列表过滤、金额格式化这类计算结果应该用 derivedStateOf 缓存,避免每次重组都重新算一遍;纯 UI 展示数据则可以直接读取,不需要额外包装。调试重组时,可以用 Layout Inspector 的 Compose 面板看重组计数,如果某个高频交互导致整页大范围重组,通常说明状态提升的层级放低了,把状态上移、把无状态组件下沉,是治理重组问题最有效的一招。