跳到主要内容

Jetpack Compose 深入面经

Jetpack Compose 核心知识

1. 重组原理

什么时候会重组?

  • 状态变化时,依赖该状态的 Composable 会重组
  • Compose 通过水平比较(Structural Equality)决定是否跳过重组

如何避免不必要的重组

// 1. @Stable / @Immutable 标记类,告知 Compose 安全跳过
@Stable // 所有 public property 变化时都通知 Compose
data class User(val name: String, val age: Int)

// 2. remember 缓存计算结果
val sortedList = remember(list) { list.sorted() }

// 3. derivedStateOf 对 state 派生新 state
val isScrolled = remember { derivedStateOf { scrollState.value > 0 } }

// 4. key() 给列表项设置稳定标识
Column {
items.forEach { item ->
key(item.id) { // 避免内容相同时不必要的重组
ItemComposable(item)
}
}
}

2. 状态管理

状态提升(State Hoisting)

// 非受控组件(自己持有状态)
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
}

// 受控组件(状态提升到调用者)
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) { Text("$count") }
}

// 调用者
@Composable
fun ParentScreen() {
var count by remember { mutableStateOf(0) }
Counter(count = count, onIncrement = { count++ })
}

ViewModel 与 Compose 集成

@Composable
fun OrderScreen(viewModel: OrderViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is Event.NavigateToSuccess -> navController.navigate("success")
is Event.ShowError -> showSnackbar(event.msg)
}
}
}

when (uiState) {
is Loading -> CircularProgressIndicator()
is Success -> OrderContent((uiState as Success).order)
is Error -> ErrorView((uiState as Error).message)
}
}

3. 布局与修饰器

// Modifier 顺序很重要!
Box(
Modifier
.size(100.dp) // 先设大小
.background(Color.Blue) // 再设背景(背景在内容区域内)
.padding(8.dp) // 再设内边距
.clickable { } // 点击区域包含 padding
)

// Modifier 和 UI 组件顺序的弱和强规则
// padding -> background 让内边距内没有背景
Text(
text = "Hello",
modifier = Modifier
.background(Color.Red) // 背景包含 padding 内容
.padding(16.dp)
)

自定义 Layout

@Composable
fun MyCustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(content = content, modifier = modifier) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.maxOf { it.width }
val height = placeables.sumOf { it.height }
layout(width, height) {
var y = 0
placeables.forEach {
it.placeRelative(0, y)
y += it.height
}
}
}
}

4. 动画系统

// 状态动画
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = tween(durationMillis = 300)
)

// 可见性动画
AnimatedVisibility(
visible = visible,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically()
) {
MyContent()
}

// 转场动画
AnimatedContent(
targetState = currentPage,
transitionSpec = { fadeIn() togetherWith fadeOut() }
) { page ->
PageContent(page)
}

// 共享元素过渡
// Screen A
Box(Modifier.sharedElement(rememberSharedContentState("image"), ...) ) {
Image(...)
}
// Screen B 中同样的 sharedElement key 就会自动过渡

5. 性能优化

// 1. 长列表用 LazyColumn
LazyColumn {
items(items = list, key = { it.id }) { item -> // key 必须提供
ItemCard(item)
}
}

// 2. 避免在 composable 中读取 MutableList
// bad: list.forEach { ... } -> 无法追踪变化
// good: val items by rememberUpdatedState(newItems)

// 3. 使用 Baseline Profiles 提升启动性能
// 在 macrobenchmark 中定义核心路径,编译器预编译这些类

// 4. 不要在 Composable 中做老不必要的异步工作
// bad: 在 composable 中直接 launch
// good: 使用 LaunchedEffect(key) { ... }

// 5. 提副本加载验证
CompositionLocal

6. Compose 中的副作用

// LaunchedEffect:进入居和成立时执行,key 变化时重新执行
LaunchedEffect(userId) {
viewModel.loadUser(userId) // 副作用都在协程中执行
}

// SideEffect:每次重组成功后执行,不可以挂起
SideEffect {
analytics.reportScreen("OrderScreen") // 同步,安全访问外部系统
}

// DisposableEffect:组件离开时需清理
DisposableEffect(sensor) {
sensor.start()
onDispose { sensor.stop() } // 离开成居时执行
}

// rememberCoroutineScope:获取协程作用域,用于事件回调中
val scope = rememberCoroutineScope()
Button(onClick = { scope.launch { doSomething() } }) { ... }

7. 资深面试题

  • Compose 与传统 View 的格局算法比较?
    • 传统 View:Measure(36项 spec) -> Layout -> Draw,可多次 Measure
    • Compose:单次 Measure(展开 Composable) -> 分配大小 -> 放置 -> 绘制
    • Compose 由设计上保证单次 Measure,性能更可预测
  • remember 在配置变化后是否会清除?
    • 默认会清除,用 rememberSaveable 写入 Bundle 保持状态
    • 复杂对象用 Saver 接口自定义序列化
  • derivedStateOf 和 remember { ... } 的区别?
    • remember 缓存结果,依赖的 key 变化时重新计算
    • derivedStateOf 用于导出 state,内部 state 变化时才重算,避免不必要重组
    • derivedStateOf 应包在 remember { }
  • 如何在 Compose 中使用传统 View?
    • AndroidView { context -> CustomView(context).apply { ... } }
    • AndroidViewBinding 使用 ViewBinding
    • setViewCompositionStrategy 管理成居策略