Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Compose 深水区

Compose 深水区考的不是“会写 Text/Button”,而是你是否理解重组、稳定性、Snapshot、Modifier、LazyColumn 性能和测试,能不能把声明式 UI 写得稳定、可测、不卡。

一、Recomposition: 状态读取驱动局部重组

Compose 的核心是 UI = f(State)。当某个 Composable 在组合阶段读取了 State,这个读取关系会被记录;State 改变后,相关范围进入重组。

  • 重组是重新执行 Composable,不是重新创建整个 Activity/View 树。
  • 重组可能很频繁,Composable 必须幂等、无副作用、执行快。
  • 不要在 Composable 体内直接发网络请求、写数据库、改全局变量。
  • 把状态读取推迟到最小范围,减少被重组的 UI 面积。
@Composable
fun ProfileScreen(state: ProfileState, onRetry: () -> Unit) {
    when {
        state.loading -> Loading()
        state.error != null -> ErrorView(state.error, onRetry)
        else -> ProfileContent(state.user)
    }
}

二、Stability 与 Skippable

Compose 编译器会判断参数是否稳定,稳定且参数未变化时,Composable 可以被跳过(skippable)。

类型/写法对跳过的影响建议
不可变 data class(val 字段)更容易稳定UI State 尽量不可变
List<T> 接口常被视为不稳定用不可变集合或稳定 wrapper
var 的普通 class不稳定改为 State 驱动或不可变模型
lambda 每次新建可能导致参数变化必要时 remember 或上提

怎么答:优化 Compose 性能不是“少写 Composable”,而是让状态和参数稳定,让框架能精准重组和跳过。

三、remember / rememberSaveable / derivedStateOf

remember 把值保存在 Composition 中,重组不丢;rememberSaveable 额外通过 Bundle/Saver 支持配置变更和进程恢复;derivedStateOf 用于从一个或多个 State 派生计算结果,只有派生结果变化时才通知。

val listState = rememberLazyListState()
val showTopButton by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 3 }
}

使用边界:

  • remember:缓存计算结果、对象实例、滚动状态等组合内状态。
  • rememberSaveable:输入框、tab、筛选条件等需要旋转后恢复的轻量状态。
  • derivedStateOf:高频变化中只关心派生布尔/分组结果,如滚动位置。
  • 不要滥用 derivedStateOf,普通字符串拼接/低频计算没有必要。

四、Snapshot 系统

Compose 的 mutableStateOf 背后是 Snapshot 状态系统。它记录状态读取和写入,在写入提交后通知受影响的组合范围。

  • Snapshot 让 Compose 能知道“谁读了这个状态”。
  • 状态写入应发生在主线程或受 Snapshot 管理的上下文中,避免并发写冲突。
  • snapshotFlow { } 可以把 Compose State 读取转换成 Flow,适合观察滚动状态等。
  • 不要直接修改普通可变集合后期待 UI 更新;要替换 State 值或使用 Snapshot-aware 集合。
LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .collect { index -> /* analytics or load trigger */ }
}

五、Modifier 顺序与布局语义

Modifier 是链式包装,顺序会改变测量、绘制、点击区域。

写法效果
background().padding()背景覆盖 padding 外层区域,内容向内缩
padding().background()只有 padding 后的内部区域有背景
clickable().padding()点击区域包含 padding 前的范围
padding().clickable()点击区域通常只覆盖 padding 后内容

怎么落地:先想“尺寸/约束”,再想“点击/语义”,最后想“绘制”;可访问性相关 semantics 要放在能覆盖正确节点的位置。

六、LazyColumn 性能

LazyColumn 不是所有问题的自动解,关键是 key、contentType、状态位置和 item 复杂度。

  • 给稳定业务 ID 作为 key,避免插入/删除导致 item 状态错位。
  • 使用 contentType 帮助复用相同类型 item。
  • 避免在 item lambda 里做重计算、创建大对象或直接收集 Flow。
  • 分页加载用列表滚动状态 + derivedStateOf/Paging,避免每帧触发请求。
  • item 内状态要么跟业务 ID 绑定,要么上提到 ViewModel。
LazyColumn(state = listState) {
    items(
        items = users,
        key = { it.id },
        contentType = { "user" },
    ) { user ->
        UserRow(user)
    }
}

七、View 互操作与生命周期收集

Compose 和 View 混用在迁移期很常见。

  • Compose 中嵌 View:用 AndroidView,在 update 中同步参数,不要每次重组都重新创建重对象。
  • View 中嵌 Compose:用 ComposeView.setContent,并设置合适的 ViewCompositionStrategy
  • 收集 Flow:优先用 collectAsStateWithLifecycle(),避免后台生命周期仍持续收集。
  • 副作用:进入组合加载用 LaunchedEffect(key),注册监听用 DisposableEffect(key) 清理。
场景推荐 API注意点
Compose 嵌地图/广告 ViewAndroidViewfactory 创建,update 更新
Fragment 嵌 ComposeComposeView销毁 View 时释放 Composition
Flow 渲染 UIcollectAsStateWithLifecycle需要 lifecycle-runtime-compose
注册监听器DisposableEffectonDispose 反注册

八、Compose Testing

Compose 测试关注语义树而不是具体 View ID。

  • createComposeRule() 设置内容。
  • 通过 onNodeWithTextonNodeWithTagonNodeWithContentDescription 查找节点。
  • 给关键节点设置 Modifier.testTag()
  • 对异步状态变化使用 waitUntil 或测试时注入可控 Dispatcher/Repository。
composeTestRule.setContent { LoginScreen(state, onSubmit = {}) }
composeTestRule.onNodeWithTag("login_button").assertIsDisplayed()

怎么答:Compose 测试不是截图测试优先,而是验证语义、状态渲染和用户交互;业务逻辑仍应在 ViewModel/UseCase 里用普通单元测试覆盖。


高频面试题

Q1:什么会触发 Compose 重组? Composable 在组合阶段读取的 State 变化会触发相关范围重组。重组是局部重新执行函数,不是整棵 UI 全量重建。Composable 要幂等、无副作用、快速执行。

Q2:稳定性和 skippable 是什么? 如果 Composable 参数稳定且值没变,编译器/运行时可以跳过它,减少重组成本。不稳定类型如可变 class、普通 List 接口会降低跳过概率,应使用不可变 UI State 和稳定集合。

Q3:remember、rememberSaveable、derivedStateOf 怎么区分? remember 保留组合内状态,重组不丢但配置变更会丢;rememberSaveable 通过 Bundle/Saver 支持恢复;derivedStateOf 用于从高频状态派生低频结果,结果不变时不触发下游重组。

Q4:Snapshot 系统解决什么问题? 它跟踪 Compose State 的读取和写入,让框架知道哪些组合范围依赖某个状态,写入提交后精准通知重组。普通可变集合不受 Snapshot 自动追踪,直接 mutate 可能不刷新 UI。

Q5:LazyColumn 怎么优化? 提供稳定 key 和 contentType,避免 item lambda 重计算和直接收集 Flow,把状态按业务 ID 管理,分页触发用 derivedStateOf/snapshotFlow 控制频率,不要每次滚动都发请求。

易错点 / 追问

  • 在 Composable 函数体里直接发请求或写外部状态,会因重组重复执行。
  • remember 不能跨配置变更保存状态,输入框/筛选条件要考虑 rememberSaveable 或 ViewModel。
  • Modifier 顺序会改变背景、padding、点击区域和语义范围,不是随便链式调用。
  • 普通 mutableList.add() 不一定触发 UI 更新,应替换 State 或使用 Snapshot-aware 状态集合。
  • LazyColumn 不加稳定 key 时,插入/删除可能导致 item 状态错位和不必要重组。