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 在某状态下长什么样“,状态变了框架自动重绘。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.immutableImmutableList

三、状态管理

  • 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.currentMaterialTheme
  • Modifier:装饰 Composable(尺寸、padding、点击、背景)。顺序敏感:padding().background()background().padding() 效果不同(前者内边距区域无背景色)。Modifier 是链式不可变的。

六、三大阶段

Compose 渲染分三阶段:

  1. Composition(组合):执行 Composable,构建/更新 UI 树(发射什么)。
  2. Layout(布局):测量 + 摆放(多大、放哪)。
  3. 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)。好处:单一数据源、可复用、可测试、可被多处控制。