UI 体系 - Jetpack Compose ★
你的重点短板,且 2024-2025 中级面试几乎必问。 Compose 是 Android 声明式 UI 的未来,新项目首选。即便面试官项目还在用 View,也常问 Compose 的理解。
一、声明式 UI 思想
- 命令式(View):你持有 View 引用,手动 findViewById、setText、setVisibility 去“改“UI。
- 声明式(Compose):你描述“UI 在某状态下长什么样“,状态变了框架自动重绘。
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 要无副作用、幂等、快。
- 执行顺序不保证、可能并行。
- 不要在 Composable 体内直接做副作用(网络、写变量),要用副作用 API。
稳定性(Stability)
- 编译器判断类型是否
@Stable/@Immutable。不稳定类型(如List接口、含 var 的 class)会导致无法跳过,引发不必要重组。 - 优化:用
data class+ 不可变属性、kotlinx.collections.immutable的ImmutableList。
三、状态管理
remember { }:在重组间记住值(存在 Composition 中),重组不重置。mutableStateOf():创建可观察状态,读取它的 Composable 会在它变化时重组。remember { mutableStateOf(x) }:最常见组合。rememberSaveable还能跨配置变更/进程恢复。
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
- 状态提升(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。 - Modifier:装饰 Composable(尺寸、padding、点击、背景)。顺序敏感:
padding().background()与background().padding()效果不同(前者内边距区域无背景色)。Modifier 是链式不可变的。
六、三大阶段
Compose 渲染分三阶段:
- Composition(组合):执行 Composable,构建/更新 UI 树(发射什么)。
- Layout(布局):测量 + 摆放(多大、放哪)。
- Drawing(绘制):画到屏幕。
理解阶段有助于性能优化:只改绘制层的东西(如颜色)不必触发重组——用 lambda 形式的 Modifier(如 graphicsLayer { }、drawBehind)可跳过 Composition/Layout,直接在 Draw 阶段更新。
七、与 View 互操作 & 性能
- Compose 中嵌 View:
AndroidView { }。 - View 中嵌 Compose:
ComposeView.setContent { }。 - 性能优化要点:
- 用稳定类型,避免不必要重组。
- LazyColumn 给
key提升 diff 效率。 - 把状态读取推迟到最小范围(defer reads),用 lambda Modifier。
- 避免在 Composable 里做重计算(用 remember 缓存)。
@Stable/@Immutable标注帮助编译器跳过。
高频面试题
Q1:Compose 和传统 View 的本质区别? View 是命令式、保留模式(retained,持有可变 View 树,手动改);Compose 是声明式、即时模式思想(描述 UI=f(state),状态变自动重组)。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:为什么会发生不必要的重组?怎么优化? 参数类型不稳定(List 接口、含 var 的类)导致无法跳过。优化:用不可变类型、@Stable/@Immutable、derivedStateOf、给 LazyColumn 加 key、defer state reads。
Q6:Compose 的三个阶段是什么? Composition(发射 UI 树)、Layout(测量摆放)、Drawing(绘制)。只影响绘制的变化可用 lambda Modifier 跳过前两阶段。
Q7:Modifier 顺序为什么重要? Modifier 链按顺序应用,每个包裹前一个。padding 在 background 前后效果不同。size、padding、clickable 顺序都会改变最终表现。
Q8:状态提升是什么?为什么重要? 把状态从 Composable 内移到调用方,使 Composable 无状态(接 value + onValueChange)。好处:单一数据源、可复用、可测试、可被多处控制。