Compose 与 View 体系的根本差异

传统 View 体系里,界面是一棵可变的对象树:你拿到控件引用,再调用方法改属性。Compose 把这件事反过来了——界面是函数,输入状态、输出 UI 描述,状态变化时框架重新执行函数并计算最小更新。

这个转变带来三个直接好处:不用再写 findViewById 与 XML;状态与界面之间的同步由框架保证,减少了“数据变了但界面没更新”的 bug;组件天然是函数,可以提取、组合、单独预览与测试。

在 Compose 里,你写的是“界面应该是什么样”,而不是“界面要怎么变成这样”。所有技巧都围绕这一点展开。

代价是学习曲线:重组、状态提升、副作用这些概念必须先理解,否则会出现“状态莫名其妙丢了”或者“界面疯狂刷新”的问题。

@Composable 与重组

用 @Composable 标注的函数即一个界面单元。它看起来像普通函数,但有两个特殊约束:不应该有副作用(不能在这里发请求、写数据库),并且必须足够快,因为它会被反复调用。

“重组”(Recomposition)是框架的核心机制:当函数读取的状态发生变化时,Compose 会重新调用受影响的 @Composable 函数,并跳过输入未变化的部分。理解这一点后,优化方向就清楚了——让重组范围尽可能小。

@Composable
fun Counter() {
    // remember 让状态在重组之间保持;mutableStateOf 让它可以被观察
    var count by remember { mutableStateOf(0) }

    Column(Modifier.padding(16.dp)) {
        Text("当前计数:$count", fontSize = 20.sp)
        Button(onClick = { count++ }) { Text("加一") }
    }
}

注意 by 委托与 rememberSaveable 的区别:设备旋转或进程重建时,remember 的值会丢失,需要跨配置变更保留的状态应使用 rememberSaveable。

状态:remember 与状态提升

状态提升(State Hoisting)是 Compose 里最重要的一条设计原则:把状态从组件内部提到调用方,组件只接收值并通过回调上报变化。这样组件变成纯粹无状态的,可复用、可预览、可测试。

状态管理的四个原则
  • 单一数据源同一份状态只放在一个地方,避免两个副本互相不同步。
  • 状态提升无状态组件更易测试;有状态组件只负责持有与转发。
  • 可观察类型用 mutableStateOf / StateFlow,普通变量变化不会触发刷新。
  • 按需 remember只有需要跨重组保留的值才用 remember,计算结果可用 remember(key)。
Compose 示意:半透明圆角 UI 组件方块像拼图一样彼此咬合,组合成完整的手机界面轮廓
可组合的界面:每个组件职责单一,拼接起来就是完整页面。

实践建议是“状态往下沉,事件往上抛”:数据流向下、事件流向上,整个页面的数据流是单向的。做到这一点,界面逻辑会变得非常容易推理。

布局与修饰符 Modifier

Compose 的布局由基础容器构成:Column(纵向)、Row(横向)、Box(层叠)、以及 LazyColumn / LazyRow(惰性列表)。样式则通过 Modifier 链式设置。

Modifier 有两个容易踩坑的地方。第一,顺序有意义:Modifier.padding(16.dp).background(Color.Blue) 与 Modifier.background(Color.Blue).padding(16.dp) 效果不同,前者背景不含内边距,后者包含。第二,不要复用同一个 Modifier 实例去修改,链式调用本质是创建新对象,把它当不可变值使用。

常用的尺寸与对齐修饰符包括 fillMaxSize、fillMaxWidth、size、width、height、padding、weight(在 Row/Column 中分配剩余空间)。weight 与 View 体系的 layout_weight 类似,是实现自适应的主力工具。

建议把公共样式抽成扩展函数,例如 fun Modifier.cardStyle() = this.clip(RoundedCornerShape(16.dp)).background(Color.White).padding(16.dp),既统一又简洁。

列表:LazyColumn 与键

长列表必须用 LazyColumn,它只组合可见区域的项,滚动时按需创建与回收。与普通 Column 的区别就像 RecyclerView 与 LinearLayout 的区别。

@Composable
fun UserList(users: List<User>, onClick: (User) -> Unit) {
    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        items(items = users, key = { it.id }) { user ->
            UserRow(user = user, onClick = { onClick(user) })
        }
    }
}

key 参数非常关键:它让 Compose 在数据顺序变化或插入删除时正确复用组件,并保留各行的内部状态(如滚动位置、展开状态)。不指定 key 时,Compose 按位置匹配,列表头部插入一条就会导致所有行的状态错位——这是最常见的一类“诡异 bug”。

副作用:LaunchedEffect 与 rememberCoroutineScope

@Composable 函数应该保持纯粹,发请求、注册监听、启动协程这类动作属于“副作用”,必须放在专门的 API 里,否则会在每次重组时重复执行。

  • LaunchedEffect(key):进入组合时启动协程,key 变化时取消并重启,离开组合时自动取消。用于加载数据。
  • rememberCoroutineScope():拿到与组合生命周期绑定的作用域,用于在点击事件里启动协程。
  • DisposableEffect(key):需要清理的注册(如监听器、广播),在 onDispose 中注销。
  • SideEffect:每次成功重组后执行一次,用于把状态同步给非 Compose 代码。

判断准则:如果一段代码在“重组”时不应该重复执行,它就必须放进副作用 API。没有例外。