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

UI 体系 - Jetpack Compose ★

你的重点短板, 且 2024-2025 中级面试几乎必问. Compose 是 Android 声明式 UI 的未来, 新项目首选. 即便面试官项目还在用 View, 也常问 Compose 的理解.

一, 声明式 UI 思想

  • 命令式 (View): 你持有 View 引用, 手动 findViewById, setText, setVisibility 去 “改” UI.
  • 声明式 (Compose): 你描述 “UI 在某状态下长什么样”, 状态变了框架自动重组 (recomposition, 重跑受影响的 Composable). UI = f(State).
@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name")   // 不持有引用,name 变了自动重组
}

二,@Composable 与重组 (Recomposition)

  • @Composable 函数不返回 UI 对象, 而是向 Composition (组合树) 发射 (emit) 描述节点.
  • 直觉上可把 @Composable 理解为 “带有组合上下文的函数”:编译器会改写调用以便框架记录调用位置, 参数和状态读取, 从而决定哪里需要重组; 具体改写细节属于实现层面, 面试重点是理解 “状态读取驱动局部重组”.
  • 重组: 当 Composable 读取的 State 变化时, 框架重新执行受影响的 Composable 来更新 UI.
  • 重组特性 (必须理解, 易踩坑):
    • 可能频繁发生, 所以 Composable 要无副作用, 幂等, 快速.
    • 执行顺序不保证, 可能并行.
    • 不要在 Composable 体内直接做副作用 (网络, 写变量), 要用副作用 API.

稳定性, strong skipping, compiler reports 与性能测量属于 Compose 深水区; 本篇只要求先写出正确, 可读, 可测试的页面.

三, 状态管理

  • remember { }: 在重组间记住值 (存在 Composition 中), 重组不重置.
  • mutableStateOf(): 创建可观察状态, 读取它的 Composable 会在它变化时重组.
  • remember { mutableStateOf(x) }: 最常见组合. rememberSaveable 还能跨配置变更 / 进程恢复.
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
  • rememberSaveable 自定义 Saver: 不能直接存入 Bundle 的类型用 rememberSaveable(saver = ...) 提供 Saver(save = { ... }, restore = { ... }). 示意: rememberSaveable(saver = Saver(save = { it.id }, restore = { findById(it) })) { mutableStateOf(Task()) }, 让非 Parcelable 对象也能跨配置恢复.
  • lambda 捕获陷阱: remember { count } 的初值 lambda 只在首次组合执行, 捕获的是当时旧值; 需要随 count 变化重建时应把 count 放进 key (remember(count) { ... }), 长生命周期对象里要用 rememberUpdatedState 拿最新值.
  • 状态提升 (State Hoisting): 把状态移到调用者, Composable 变成无状态 (stateless), 通过参数接收 value + 回调 onValueChange. 利于复用, 测试, 单一数据源.

四, 副作用 API (Side Effects)

副作用是 “在 Composable 之外发生的影响”.因为重组随时可能发生, 副作用必须用专门 API 管理生命周期:

  • LaunchedEffect(key): 进入组合时启动一个协程, key 变化时取消重启, 离开组合时取消. 用于一次性挂起操作 (加载数据, 监听 Flow).
  • rememberCoroutineScope(): 拿到一个绑定组合生命周期的 scope, 在事件回调 (如 onClick) 里启动协程.
  • DisposableEffect(key): 需要清理的副作用 (注册 / 反注册监听器), 提供 onDispose {}.
  • SideEffect: 每次重组成功后执行, 用于把 Compose 状态同步给非 Compose 代码.
  • derivedStateOf: 从其他 state 派生计算值, 只有结果变化才触发重组 (避免过度重组, 如根据滚动位置算 “是否显示回顶按钮”).
  • rememberUpdatedState: 在长生命周期 effect 中引用最新值而不重启 effect.
  • produceState / snapshotFlow: State 与 Flow 互转.
LaunchedEffect(userId) {              // userId 变才重新加载
    viewModel.load(userId)
}

五, CompositionLocal 与 Modifier

  • CompositionLocal: 隐式向下传递数据 (主题, Context), 避免逐层传参. 如 LocalContext.current, MaterialTheme.
  • compositionLocalOf 追踪读取: 某 Composable 读取它后, 值变化会触发读取处重组; staticCompositionLocalOf 不追踪读取, 值变化时使 Provider 子树整体重组, 适合几乎不变的默认值 (如主题).
  • Provider 边界: CompositionLocalProvider(LocalX provides v) { ... } 只影响其子树, 子树外读到的仍是外层值; Local 定义应在文件顶层, 不要在重组路径上创建新 Local.
  • Modifier: 装饰 Composable (尺寸, padding, 点击, 背景).顺序敏感: padding().background() 与 background().padding() 效果不同 (前者内边距区域无背景色). Modifier 是链式不可变的.

六, 三大阶段

Compose 渲染分三阶段:

  1. Composition (组合): 执行 Composable, 构建 / 更新 UI 树 (发射什么).
  2. Layout (布局): 测量 + 摆放 (多大, 放哪).
  3. Drawing (绘制): 画到屏幕.

三个阶段用于建立渲染直觉; 重组范围, 跳过条件和性能诊断见 Compose 深水区.

七, 常用布局, Material 3 与完整页面

Column 纵向摆放, Row 横向摆放, Box 用于叠放; Scaffold 提供 top bar, bottom bar, snackbar 和内容 padding 的页面骨架; LazyColumn 只组合可见附近元素, 适合动态长列表. Material 3 组件通过 MaterialTheme 读取颜色, 排版和形状.

@Composable fun AvatarWithBadge() = Box {
    Icon(Icons.Default.Person, contentDescription = "用户")
    Text("3", Modifier.align(Alignment.TopEnd))
}

可运行示例 (工程内): 在已配置 Compose Material 3 的模块, 以 setContent { MaterialTheme { TodoPage() } } 调用. Preview 显示初始态; 运行后输入文本并点击添加, 预期列表新增一项, 输入框清空, 复选框可切换完成状态.

@Composable
fun TodoPage() {
    var input by rememberSaveable { mutableStateOf("") }
    var tasks by remember { mutableStateOf(listOf("阅读 Compose" to false)) }
    Scaffold(topBar = { TopAppBar(title = { Text("待办") }) }) { padding ->
        Column(Modifier.padding(padding).padding(16.dp)) {
            Row(verticalAlignment = Alignment.CenterVertically) {
                OutlinedTextField(input, { input = it }, label = { Text("新任务") }, modifier = Modifier.weight(1f))
                Button(onClick = { if (input.isNotBlank()) { tasks = tasks + (input.trim() to false); input = "" } }) { Text("添加") }
            }
            LazyColumn { itemsIndexed(tasks) { index, task ->
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Checkbox(task.second, { checked -> tasks = tasks.toMutableList().also { it[index] = task.first to checked } })
                    Text(task.first)
                }
            } }
        }
    }
}
@Preview(showBackground = true) @Composable
private fun TodoPagePreview() { MaterialTheme { TodoPage() } }

这里 TodoPage 拥有演示状态; 复用组件应提升状态, 例如 TaskRow(task, checked, onCheckedChange), 让调用方持有状态和业务 ID. 列表来自服务端或可删除时, 不能用 index 作稳定 key, 应使用业务 ID.

八, 与 View 互操作

  • Compose 中嵌 View: AndroidView(factory = { ctx -> View(ctx) }, update = { view, state -> ... }), factory 只在首次组合创建 View 一次, update 在每次重组同步参数, 不要在 factory 里放会变的状态.
  • View 中嵌 Compose: ComposeView.setContent { }, 并设置合适的 ViewCompositionStrategy.
  • ViewCompositionStrategy 决定 Composition 何时销毁: 默认 DisposeOnViewTreeLifecycleDestroyed (View attach 到带 Lifecycle 的树, 其销毁时释放); 也可用 DisposeOnDetachedFromWindow (脱离窗口即释放) 或 DisposeOnLifecycleDestroyed(lifecycle) (显式绑定 Lifecycle).
  • 性能诊断, 稳定性与跳过规则统一见 Compose 深水区.

高频面试题

Q1: Compose 和传统 View 的本质区别? View 是命令式, 保留模式 (retained, 持有可变 View 树, 手动改); Compose 是声明式, 同样维护持续存在的组合树 (retained Composition), 状态变化时只增量重组受影响部分, 不是每帧全量重绘的即时模式 (immediate mode). Compose 无 findViewById, 无 XML, Kotlin 写 UI.

Q2: 什么是重组? 它有什么注意事项? State 变化时重新执行受影响的 Composable 更新 UI. 注意: 可能频繁/并行/乱序执行, 所以 Composable 必须无副作用, 幂等, 快, 副作用要用专门 API.

Q3: remember 和 rememberSaveable 区别? remember 在重组间保留值, 但配置变更 (旋转)/进程重建会丢失; rememberSaveable 额外通过 Bundle 保存, 能跨配置变更恢复 (类型需可 Parcelize).

Q4: LaunchedEffect 和 rememberCoroutineScope 区别? LaunchedEffect 在组合期启动协程, 随 key 重启, 离开自动取消, 用于进入界面就执行的副作用; rememberCoroutineScope 返回 scope, 在事件回调里手动启动协程.

Q5: 为什么要让 Composable 保持无副作用? 因为状态变化可使受影响范围再次执行, 函数体内直接发请求, 写数据库或修改外部变量会被重复执行. 把这类工作交给 LaunchedEffect, 事件回调或 ViewModel; 具体重组诊断见 Compose 深水区.

Q6: Compose 的三个阶段是什么? Composition 发射 UI 树, Layout 测量和摆放, Drawing 绘制内容. 它们帮助理解一次 UI 更新的路径; 性能诊断与跳过规则见 Compose 深水区.

Q7: Modifier 顺序为什么重要? Modifier 链按顺序应用, 每个包裹前一个. padding 在 background 前后效果不同. size, padding, clickable 顺序都会改变最终表现.

Q8: 状态提升是什么? 为什么重要? 把状态从 Composable 内移到调用方, 使 Composable 无状态 (接 value + onValueChange).好处: 单一数据源, 可复用, 可测试, 可被多处控制.

易错点 / 追问

  • remember { } 的初值 lambda 只在首次组合执行, 捕获的是当时旧值: 需要随状态重建时把变化值放进 key, 长生命周期 effect 中要用 rememberUpdatedState.
  • 不要在 Composable 体内直接做副作用 (网络, 写库, 改外部变量): 重组可能反复执行函数体, 副作用要交给 LaunchedEffect / DisposableEffect 或事件回调.
  • LaunchedEffect 的 key 一变就取消重启: 把每次重组都新建的对象放进去会导致无限重启; key 应使用稳定标识 (ID 或业务值).
  • rememberSaveable 只能保存 Bundle 兼容类型: 自定义对象必须提供 Saver, 否则配置变更后恢复失败.
  • Modifier 顺序敏感: padding().background() 与 background().padding() 表现不同; 说不清顺序时按 “每个 Modifier 包裹前一个” 推导.
  • AndroidView 的 factory 只在首次组合执行: 把会变的状态放进 factory 是常见错误, 同步参数要放 update.
  • View 中嵌 Compose 时 ViewCompositionStrategy 选错会导致组合不销毁或过早销毁: 默认 DisposeOnViewTreeLifecycleDestroyed 覆盖大多数场景, 脱离 Lifecycle 树时改用 DisposeOnDetachedFromWindow.