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 渲染分三阶段:
- Composition (组合): 执行 Composable, 构建 / 更新 UI 树 (发射什么).
- Layout (布局): 测量 + 摆放 (多大, 放哪).
- 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.