Jetpack Compose 面试知识精读(上半部分:知识结构)
阅读顺序即学习顺序。从"为什么"→"怎么用"→"底层原理"。忘记时从任意一节读起,每节末尾有「一句话总结」。
目录(上半部分)
第一阶段:心智模型
- 声明式 UI vs 命令式 UI
- Compose 三大阶段:Composition → Layout → Draw
- 重组(Recomposition)是什么
第二阶段:状态与重组机制
-
State / MutableState / remember / rememberSaveable
-
快照系统(Snapshot)与状态读取追踪
-
重组作用域与智能重组(Smart Recomposition)
-
稳定性(Stability):Stable / Unstable / Immutable
第三阶段:副作用与生命周期
-
副作用 API 全家桶
-
副作用与组合生命周期(key 的作用)
第四阶段:布局与绘制
-
布局原理:Constraints / MeasurePolicy
-
Modifier 的本质与顺序敏感性
-
自定义 Layout 与 SubcomposeLayout
第五阶段:性能与底层
-
Slot Table 与 Composition 数据结构
-
重组优化手段
-
Compose 编译器插件做了什么
第六阶段:工程实践
-
与 View 互操作
-
状态提升与单向数据流
-
常见陷阱汇总
第一阶段:心智模型
1. 声明式 UI vs 命令式 UI
概念
- 命令式(传统 View):你拿着 UI 对象的引用,手动一步步改它。"找到 TextView → setText → 如果变了再 setVisibility"。
- 声明式(Compose):你只描述"当前状态下的 UI 长什么样",状态变了,框架负责把 UI 更新成新描述的样子。你不持有 UI 对象引用。
图示
#mermaid-svg-RXc5qY1JY0Mpzjxs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RXc5qY1JY0Mpzjxs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RXc5qY1JY0Mpzjxs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RXc5qY1JY0Mpzjxs .error-icon{fill:#552222;}#mermaid-svg-RXc5qY1JY0Mpzjxs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RXc5qY1JY0Mpzjxs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RXc5qY1JY0Mpzjxs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RXc5qY1JY0Mpzjxs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RXc5qY1JY0Mpzjxs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RXc5qY1JY0Mpzjxs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RXc5qY1JY0Mpzjxs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RXc5qY1JY0Mpzjxs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RXc5qY1JY0Mpzjxs .marker.cross{stroke:#333333;}#mermaid-svg-RXc5qY1JY0Mpzjxs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RXc5qY1JY0Mpzjxs p{margin:0;}#mermaid-svg-RXc5qY1JY0Mpzjxs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RXc5qY1JY0Mpzjxs .cluster-label text{fill:#333;}#mermaid-svg-RXc5qY1JY0Mpzjxs .cluster-label span{color:#333;}#mermaid-svg-RXc5qY1JY0Mpzjxs .cluster-label span p{background-color:transparent;}#mermaid-svg-RXc5qY1JY0Mpzjxs .label text,#mermaid-svg-RXc5qY1JY0Mpzjxs span{fill:#333;color:#333;}#mermaid-svg-RXc5qY1JY0Mpzjxs .node rect,#mermaid-svg-RXc5qY1JY0Mpzjxs .node circle,#mermaid-svg-RXc5qY1JY0Mpzjxs .node ellipse,#mermaid-svg-RXc5qY1JY0Mpzjxs .node polygon,#mermaid-svg-RXc5qY1JY0Mpzjxs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RXc5qY1JY0Mpzjxs .rough-node .label text,#mermaid-svg-RXc5qY1JY0Mpzjxs .node .label text,#mermaid-svg-RXc5qY1JY0Mpzjxs .image-shape .label,#mermaid-svg-RXc5qY1JY0Mpzjxs .icon-shape .label{text-anchor:middle;}#mermaid-svg-RXc5qY1JY0Mpzjxs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RXc5qY1JY0Mpzjxs .rough-node .label,#mermaid-svg-RXc5qY1JY0Mpzjxs .node .label,#mermaid-svg-RXc5qY1JY0Mpzjxs .image-shape .label,#mermaid-svg-RXc5qY1JY0Mpzjxs .icon-shape .label{text-align:center;}#mermaid-svg-RXc5qY1JY0Mpzjxs .node.clickable{cursor:pointer;}#mermaid-svg-RXc5qY1JY0Mpzjxs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RXc5qY1JY0Mpzjxs .arrowheadPath{fill:#333333;}#mermaid-svg-RXc5qY1JY0Mpzjxs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RXc5qY1JY0Mpzjxs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RXc5qY1JY0Mpzjxs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RXc5qY1JY0Mpzjxs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RXc5qY1JY0Mpzjxs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RXc5qY1JY0Mpzjxs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RXc5qY1JY0Mpzjxs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RXc5qY1JY0Mpzjxs .cluster text{fill:#333;}#mermaid-svg-RXc5qY1JY0Mpzjxs .cluster span{color:#333;}#mermaid-svg-RXc5qY1JY0Mpzjxs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RXc5qY1JY0Mpzjxs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RXc5qY1JY0Mpzjxs rect.text{fill:none;stroke-width:0;}#mermaid-svg-RXc5qY1JY0Mpzjxs .icon-shape,#mermaid-svg-RXc5qY1JY0Mpzjxs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RXc5qY1JY0Mpzjxs .icon-shape p,#mermaid-svg-RXc5qY1JY0Mpzjxs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RXc5qY1JY0Mpzjxs .icon-shape .label rect,#mermaid-svg-RXc5qY1JY0Mpzjxs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RXc5qY1JY0Mpzjxs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RXc5qY1JY0Mpzjxs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RXc5qY1JY0Mpzjxs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 声明式
状态变化
重新执行 Composable 函数
生成新的 UI 描述
框架 diff 后更新 UI
命令式
状态变化
手动找到 View
手动调用 setText/setVisibility
UI 更新
举例
kotlin
// 命令式
fun updateCount(count: Int) {
textView.text = count.toString()
if (count > 10) textView.setTextColor(RED)
}
// 声明式
@Composable
fun Counter(count: Int) {
Text(
text = count.toString(),
color = if (count > 10) Color.Red else Color.Black
)
}
声明式里没有"更新"这个动作------Counter 被重新调用时,Text 自然就是新样子。"UI 是状态的函数" :UI = f(state)。
面试价值
这是理解后面一切的根:因为 UI 是函数,所以状态变化时不是"改 UI",而是"重新执行函数"------这就引出了重组 和如何高效地重新执行两个核心问题。
一句话总结:Compose 里你描述 UI 而不是操作 UI,状态变 → 函数重跑 → UI 自然更新。
2. Compose 三大阶段:Composition → Layout → Draw
概念
一次 UI 渲染在 Compose 里分成三个阶段,和传统 View 的 measure/layout/draw 类似但更清晰:
#mermaid-svg-xHjsoDt0JpYd0MyK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xHjsoDt0JpYd0MyK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xHjsoDt0JpYd0MyK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xHjsoDt0JpYd0MyK .error-icon{fill:#552222;}#mermaid-svg-xHjsoDt0JpYd0MyK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xHjsoDt0JpYd0MyK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xHjsoDt0JpYd0MyK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xHjsoDt0JpYd0MyK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xHjsoDt0JpYd0MyK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xHjsoDt0JpYd0MyK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xHjsoDt0JpYd0MyK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xHjsoDt0JpYd0MyK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xHjsoDt0JpYd0MyK .marker.cross{stroke:#333333;}#mermaid-svg-xHjsoDt0JpYd0MyK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xHjsoDt0JpYd0MyK p{margin:0;}#mermaid-svg-xHjsoDt0JpYd0MyK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xHjsoDt0JpYd0MyK .cluster-label text{fill:#333;}#mermaid-svg-xHjsoDt0JpYd0MyK .cluster-label span{color:#333;}#mermaid-svg-xHjsoDt0JpYd0MyK .cluster-label span p{background-color:transparent;}#mermaid-svg-xHjsoDt0JpYd0MyK .label text,#mermaid-svg-xHjsoDt0JpYd0MyK span{fill:#333;color:#333;}#mermaid-svg-xHjsoDt0JpYd0MyK .node rect,#mermaid-svg-xHjsoDt0JpYd0MyK .node circle,#mermaid-svg-xHjsoDt0JpYd0MyK .node ellipse,#mermaid-svg-xHjsoDt0JpYd0MyK .node polygon,#mermaid-svg-xHjsoDt0JpYd0MyK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xHjsoDt0JpYd0MyK .rough-node .label text,#mermaid-svg-xHjsoDt0JpYd0MyK .node .label text,#mermaid-svg-xHjsoDt0JpYd0MyK .image-shape .label,#mermaid-svg-xHjsoDt0JpYd0MyK .icon-shape .label{text-anchor:middle;}#mermaid-svg-xHjsoDt0JpYd0MyK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xHjsoDt0JpYd0MyK .rough-node .label,#mermaid-svg-xHjsoDt0JpYd0MyK .node .label,#mermaid-svg-xHjsoDt0JpYd0MyK .image-shape .label,#mermaid-svg-xHjsoDt0JpYd0MyK .icon-shape .label{text-align:center;}#mermaid-svg-xHjsoDt0JpYd0MyK .node.clickable{cursor:pointer;}#mermaid-svg-xHjsoDt0JpYd0MyK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xHjsoDt0JpYd0MyK .arrowheadPath{fill:#333333;}#mermaid-svg-xHjsoDt0JpYd0MyK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xHjsoDt0JpYd0MyK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xHjsoDt0JpYd0MyK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xHjsoDt0JpYd0MyK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xHjsoDt0JpYd0MyK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xHjsoDt0JpYd0MyK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xHjsoDt0JpYd0MyK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xHjsoDt0JpYd0MyK .cluster text{fill:#333;}#mermaid-svg-xHjsoDt0JpYd0MyK .cluster span{color:#333;}#mermaid-svg-xHjsoDt0JpYd0MyK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xHjsoDt0JpYd0MyK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xHjsoDt0JpYd0MyK rect.text{fill:none;stroke-width:0;}#mermaid-svg-xHjsoDt0JpYd0MyK .icon-shape,#mermaid-svg-xHjsoDt0JpYd0MyK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xHjsoDt0JpYd0MyK .icon-shape p,#mermaid-svg-xHjsoDt0JpYd0MyK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xHjsoDt0JpYd0MyK .icon-shape .label rect,#mermaid-svg-xHjsoDt0JpYd0MyK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xHjsoDt0JpYd0MyK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xHjsoDt0JpYd0MyK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xHjsoDt0JpYd0MyK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 生成/更新 UI 树描述
测量+定位, 得到每个节点尺寸位置
画到 Canvas
状态变化
Composition 组合
Layout 布局
Draw 绘制
屏幕
| 阶段 | 做什么 | 对应 API |
|---|---|---|
| Composition | 执行 @Composable 函数,构建/更新 UI 树(Slot Table) |
@Composable |
| Layout | 每个节点测量自身、确定子节点位置 | MeasurePolicy、Modifier.layout |
| Draw | 把节点画到画布 | Modifier.drawBehind、Canvas |
关键点:状态读取的位置决定跳过哪些阶段
- 状态在 Composition 阶段被读取 → 状态变化触发重组(重跑 Composition),可能连带 Layout/Draw。
- 状态只在 Layout 阶段读取(如
Modifier.offset { }的 lambda 里)→ 只重跑 Layout,跳过 Composition。 - 状态只在 Draw 阶段读取(如
Modifier.drawBehind { })→ 只重跑 Draw,跳过前两阶段。
kotlin
// 读取在 Composition:offset 变化会触发重组
Modifier.offset(x = animatedX.dp)
// 读取在 Layout:只重跑 Layout,不重组
Modifier.offset { IntOffset(animatedX.roundToInt(), 0) }
// 读取在 Draw:只重跑 Draw
Modifier.drawBehind { drawCircle(radius = animatedRadius) }
这是高级面试高频考点:"如何避免不必要的重组?" 答案之一就是把状态读取推迟到 Layout/Draw 阶段(lambda 版 Modifier + graphicsLayer/offset 的 lambda 重载)。
图示:三个阶段的可跳过性
#mermaid-svg-yBFBQLWnnR1GJQtp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yBFBQLWnnR1GJQtp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yBFBQLWnnR1GJQtp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yBFBQLWnnR1GJQtp .error-icon{fill:#552222;}#mermaid-svg-yBFBQLWnnR1GJQtp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yBFBQLWnnR1GJQtp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yBFBQLWnnR1GJQtp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yBFBQLWnnR1GJQtp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yBFBQLWnnR1GJQtp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yBFBQLWnnR1GJQtp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yBFBQLWnnR1GJQtp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yBFBQLWnnR1GJQtp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yBFBQLWnnR1GJQtp .marker.cross{stroke:#333333;}#mermaid-svg-yBFBQLWnnR1GJQtp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yBFBQLWnnR1GJQtp p{margin:0;}#mermaid-svg-yBFBQLWnnR1GJQtp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yBFBQLWnnR1GJQtp .cluster-label text{fill:#333;}#mermaid-svg-yBFBQLWnnR1GJQtp .cluster-label span{color:#333;}#mermaid-svg-yBFBQLWnnR1GJQtp .cluster-label span p{background-color:transparent;}#mermaid-svg-yBFBQLWnnR1GJQtp .label text,#mermaid-svg-yBFBQLWnnR1GJQtp span{fill:#333;color:#333;}#mermaid-svg-yBFBQLWnnR1GJQtp .node rect,#mermaid-svg-yBFBQLWnnR1GJQtp .node circle,#mermaid-svg-yBFBQLWnnR1GJQtp .node ellipse,#mermaid-svg-yBFBQLWnnR1GJQtp .node polygon,#mermaid-svg-yBFBQLWnnR1GJQtp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yBFBQLWnnR1GJQtp .rough-node .label text,#mermaid-svg-yBFBQLWnnR1GJQtp .node .label text,#mermaid-svg-yBFBQLWnnR1GJQtp .image-shape .label,#mermaid-svg-yBFBQLWnnR1GJQtp .icon-shape .label{text-anchor:middle;}#mermaid-svg-yBFBQLWnnR1GJQtp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yBFBQLWnnR1GJQtp .rough-node .label,#mermaid-svg-yBFBQLWnnR1GJQtp .node .label,#mermaid-svg-yBFBQLWnnR1GJQtp .image-shape .label,#mermaid-svg-yBFBQLWnnR1GJQtp .icon-shape .label{text-align:center;}#mermaid-svg-yBFBQLWnnR1GJQtp .node.clickable{cursor:pointer;}#mermaid-svg-yBFBQLWnnR1GJQtp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yBFBQLWnnR1GJQtp .arrowheadPath{fill:#333333;}#mermaid-svg-yBFBQLWnnR1GJQtp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yBFBQLWnnR1GJQtp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yBFBQLWnnR1GJQtp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yBFBQLWnnR1GJQtp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yBFBQLWnnR1GJQtp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yBFBQLWnnR1GJQtp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yBFBQLWnnR1GJQtp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yBFBQLWnnR1GJQtp .cluster text{fill:#333;}#mermaid-svg-yBFBQLWnnR1GJQtp .cluster span{color:#333;}#mermaid-svg-yBFBQLWnnR1GJQtp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yBFBQLWnnR1GJQtp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yBFBQLWnnR1GJQtp rect.text{fill:none;stroke-width:0;}#mermaid-svg-yBFBQLWnnR1GJQtp .icon-shape,#mermaid-svg-yBFBQLWnnR1GJQtp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yBFBQLWnnR1GJQtp .icon-shape p,#mermaid-svg-yBFBQLWnnR1GJQtp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yBFBQLWnnR1GJQtp .icon-shape .label rect,#mermaid-svg-yBFBQLWnnR1GJQtp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yBFBQLWnnR1GJQtp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yBFBQLWnnR1GJQtp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yBFBQLWnnR1GJQtp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Composition
Layout lambda
Draw lambda
状态变化
状态在哪被读?
重组 → 可能 Layout → 可能 Draw
仅 Layout + Draw
仅 Draw
一句话总结:Compose 渲染分 Composition/Layout/Draw 三阶段,状态读取越晚(越靠后阶段),跳过的阶段越多,性能越好。
3. 重组(Recomposition)是什么
概念
重组 = 当输入(状态/参数)变化时,重新执行受影响的 @Composable 函数,生成新的 UI 描述。 它不是重建整棵树,而是"智能地只重跑需要更新的部分"。
图示:重组不是全量重跑
#mermaid-svg-MKMiBIMKiHBJDIrb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MKMiBIMKiHBJDIrb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MKMiBIMKiHBJDIrb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MKMiBIMKiHBJDIrb .error-icon{fill:#552222;}#mermaid-svg-MKMiBIMKiHBJDIrb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MKMiBIMKiHBJDIrb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MKMiBIMKiHBJDIrb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MKMiBIMKiHBJDIrb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MKMiBIMKiHBJDIrb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MKMiBIMKiHBJDIrb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MKMiBIMKiHBJDIrb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MKMiBIMKiHBJDIrb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MKMiBIMKiHBJDIrb .marker.cross{stroke:#333333;}#mermaid-svg-MKMiBIMKiHBJDIrb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MKMiBIMKiHBJDIrb p{margin:0;}#mermaid-svg-MKMiBIMKiHBJDIrb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MKMiBIMKiHBJDIrb .cluster-label text{fill:#333;}#mermaid-svg-MKMiBIMKiHBJDIrb .cluster-label span{color:#333;}#mermaid-svg-MKMiBIMKiHBJDIrb .cluster-label span p{background-color:transparent;}#mermaid-svg-MKMiBIMKiHBJDIrb .label text,#mermaid-svg-MKMiBIMKiHBJDIrb span{fill:#333;color:#333;}#mermaid-svg-MKMiBIMKiHBJDIrb .node rect,#mermaid-svg-MKMiBIMKiHBJDIrb .node circle,#mermaid-svg-MKMiBIMKiHBJDIrb .node ellipse,#mermaid-svg-MKMiBIMKiHBJDIrb .node polygon,#mermaid-svg-MKMiBIMKiHBJDIrb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MKMiBIMKiHBJDIrb .rough-node .label text,#mermaid-svg-MKMiBIMKiHBJDIrb .node .label text,#mermaid-svg-MKMiBIMKiHBJDIrb .image-shape .label,#mermaid-svg-MKMiBIMKiHBJDIrb .icon-shape .label{text-anchor:middle;}#mermaid-svg-MKMiBIMKiHBJDIrb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MKMiBIMKiHBJDIrb .rough-node .label,#mermaid-svg-MKMiBIMKiHBJDIrb .node .label,#mermaid-svg-MKMiBIMKiHBJDIrb .image-shape .label,#mermaid-svg-MKMiBIMKiHBJDIrb .icon-shape .label{text-align:center;}#mermaid-svg-MKMiBIMKiHBJDIrb .node.clickable{cursor:pointer;}#mermaid-svg-MKMiBIMKiHBJDIrb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MKMiBIMKiHBJDIrb .arrowheadPath{fill:#333333;}#mermaid-svg-MKMiBIMKiHBJDIrb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MKMiBIMKiHBJDIrb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MKMiBIMKiHBJDIrb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MKMiBIMKiHBJDIrb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MKMiBIMKiHBJDIrb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MKMiBIMKiHBJDIrb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MKMiBIMKiHBJDIrb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MKMiBIMKiHBJDIrb .cluster text{fill:#333;}#mermaid-svg-MKMiBIMKiHBJDIrb .cluster span{color:#333;}#mermaid-svg-MKMiBIMKiHBJDIrb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MKMiBIMKiHBJDIrb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MKMiBIMKiHBJDIrb rect.text{fill:none;stroke-width:0;}#mermaid-svg-MKMiBIMKiHBJDIrb .icon-shape,#mermaid-svg-MKMiBIMKiHBJDIrb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MKMiBIMKiHBJDIrb .icon-shape p,#mermaid-svg-MKMiBIMKiHBJDIrb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MKMiBIMKiHBJDIrb .icon-shape .label rect,#mermaid-svg-MKMiBIMKiHBJDIrb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MKMiBIMKiHBJDIrb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MKMiBIMKiHBJDIrb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MKMiBIMKiHBJDIrb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} count 变化
跳过
跳过
Root Composable
Header 参数未变
Counter 读取了 count
Footer 参数未变
Counter 重组
上图中 count 只在 Counter 里被读取,所以只有 Counter 及其内部需要重组的子节点会重跑,Header/Footer 被跳过(skip)。
重组的触发条件
- 读取的
State值发生变化(快照系统追踪,见第 5 节)。 - 传入的参数发生变化(且参数类型可比较、Composable 可跳过,见第 6、7 节)。
- 父级重组且自己无法被跳过。
重组是"可取消、可并发、可能多次"的
- 重组可能被取消(比如中途状态又变了)。
- 重组在不同帧可能执行多次。
- 因此 Composable 函数必须是幂等、无副作用的------这正是副作用 API(第 8 节)存在的原因。
一句话总结:重组 = 输入变化时重跑受影响的 Composable,框架会跳过没变的子树;重组可取消可重复,所以 Composable 必须无副作用。
第二阶段:状态与重组机制
4. State / MutableState / remember / rememberSaveable
概念分层
#mermaid-svg-dDeVJm1Ue5O4azjJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dDeVJm1Ue5O4azjJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dDeVJm1Ue5O4azjJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dDeVJm1Ue5O4azjJ .error-icon{fill:#552222;}#mermaid-svg-dDeVJm1Ue5O4azjJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dDeVJm1Ue5O4azjJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dDeVJm1Ue5O4azjJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dDeVJm1Ue5O4azjJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dDeVJm1Ue5O4azjJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dDeVJm1Ue5O4azjJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dDeVJm1Ue5O4azjJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dDeVJm1Ue5O4azjJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dDeVJm1Ue5O4azjJ .marker.cross{stroke:#333333;}#mermaid-svg-dDeVJm1Ue5O4azjJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dDeVJm1Ue5O4azjJ p{margin:0;}#mermaid-svg-dDeVJm1Ue5O4azjJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dDeVJm1Ue5O4azjJ .cluster-label text{fill:#333;}#mermaid-svg-dDeVJm1Ue5O4azjJ .cluster-label span{color:#333;}#mermaid-svg-dDeVJm1Ue5O4azjJ .cluster-label span p{background-color:transparent;}#mermaid-svg-dDeVJm1Ue5O4azjJ .label text,#mermaid-svg-dDeVJm1Ue5O4azjJ span{fill:#333;color:#333;}#mermaid-svg-dDeVJm1Ue5O4azjJ .node rect,#mermaid-svg-dDeVJm1Ue5O4azjJ .node circle,#mermaid-svg-dDeVJm1Ue5O4azjJ .node ellipse,#mermaid-svg-dDeVJm1Ue5O4azjJ .node polygon,#mermaid-svg-dDeVJm1Ue5O4azjJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dDeVJm1Ue5O4azjJ .rough-node .label text,#mermaid-svg-dDeVJm1Ue5O4azjJ .node .label text,#mermaid-svg-dDeVJm1Ue5O4azjJ .image-shape .label,#mermaid-svg-dDeVJm1Ue5O4azjJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-dDeVJm1Ue5O4azjJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dDeVJm1Ue5O4azjJ .rough-node .label,#mermaid-svg-dDeVJm1Ue5O4azjJ .node .label,#mermaid-svg-dDeVJm1Ue5O4azjJ .image-shape .label,#mermaid-svg-dDeVJm1Ue5O4azjJ .icon-shape .label{text-align:center;}#mermaid-svg-dDeVJm1Ue5O4azjJ .node.clickable{cursor:pointer;}#mermaid-svg-dDeVJm1Ue5O4azjJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dDeVJm1Ue5O4azjJ .arrowheadPath{fill:#333333;}#mermaid-svg-dDeVJm1Ue5O4azjJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dDeVJm1Ue5O4azjJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dDeVJm1Ue5O4azjJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dDeVJm1Ue5O4azjJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dDeVJm1Ue5O4azjJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dDeVJm1Ue5O4azjJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dDeVJm1Ue5O4azjJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dDeVJm1Ue5O4azjJ .cluster text{fill:#333;}#mermaid-svg-dDeVJm1Ue5O4azjJ .cluster span{color:#333;}#mermaid-svg-dDeVJm1Ue5O4azjJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dDeVJm1Ue5O4azjJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dDeVJm1Ue5O4azjJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-dDeVJm1Ue5O4azjJ .icon-shape,#mermaid-svg-dDeVJm1Ue5O4azjJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dDeVJm1Ue5O4azjJ .icon-shape p,#mermaid-svg-dDeVJm1Ue5O4azjJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dDeVJm1Ue5O4azjJ .icon-shape .label rect,#mermaid-svg-dDeVJm1Ue5O4azjJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dDeVJm1Ue5O4azjJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dDeVJm1Ue5O4azjJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dDeVJm1Ue5O4azjJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} State 接口: 可被观察的值
MutableState: 可读写
mutate 触发读取它的 Composable 重组
remember: 在重组间保留对象
rememberSaveable: 还能在进程死亡/配置变更后恢复
State<T>:只读,Compose 能观察它的读取。MutableState<T>:可读写,mutableStateOf()创建。写.value会通知读取过它的 Composable 重组。remember { }:把对象存到当前 Composable 的 Slot Table 槽位里,重组时复用,不重新创建。rememberSaveable { }:在remember基础上,通过SaveableStateRegistry把值存进Bundle,配置变更(旋转)和进程被回收后重建时能恢复。
举例
kotlin
@Composable
fun Counter() {
// 错误:每次重组都新建,值丢失
var count by mutableStateOf(0)
// 正确:重组间保留
var count by remember { mutableStateOf(0) }
// 需要跨配置变更保留
var count2 by rememberSaveable { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
}
底层:remember 靠什么"记住"
remember 把值存在当前 Composable 对应的 Slot Table 槽位(见第 13 节)。重组时如果槽位还在且 key 匹配,就复用;否则重新执行 lambda。
rememberSaveable 的边界
- 只能保存能进
Bundle的类型(基本类型、String、Parcelable、Serializable 等),复杂对象要自定义Saver。 - 它解决的是"进程死亡/配置变更",不是"重组"------重组靠
remember就够。
一句话总结 :
State是可被观察的值,remember让它在重组间存活,rememberSaveable额外让它跨配置变更/进程死亡存活。
5. 快照系统(Snapshot)与状态读取追踪
这是 Compose 最"高级"也最容易被问倒的部分。先用直觉建立模型,再看细节。
直觉模型:Compose 给每个 State 装了"读取记录仪"
想象每个 MutableState 都有一本"谁读过我"的登记簿。当某个 Composable 执行时读了 state.value,Compose 就把"当前这个 Composable 的作用域"记到该 State 的登记簿上。之后这个 State 被写,Compose 就翻登记簿,通知所有登记过的 Composable 来重组。
重组调度器 MutableState Composable 作用域 重组调度器 MutableState Composable 作用域 #mermaid-svg-2BjggreBf9Md0lf4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2BjggreBf9Md0lf4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2BjggreBf9Md0lf4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2BjggreBf9Md0lf4 .error-icon{fill:#552222;}#mermaid-svg-2BjggreBf9Md0lf4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2BjggreBf9Md0lf4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2BjggreBf9Md0lf4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2BjggreBf9Md0lf4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2BjggreBf9Md0lf4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2BjggreBf9Md0lf4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2BjggreBf9Md0lf4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2BjggreBf9Md0lf4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2BjggreBf9Md0lf4 .marker.cross{stroke:#333333;}#mermaid-svg-2BjggreBf9Md0lf4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2BjggreBf9Md0lf4 p{margin:0;}#mermaid-svg-2BjggreBf9Md0lf4 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2BjggreBf9Md0lf4 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-2BjggreBf9Md0lf4 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2BjggreBf9Md0lf4 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-2BjggreBf9Md0lf4 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-2BjggreBf9Md0lf4 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-2BjggreBf9Md0lf4 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-2BjggreBf9Md0lf4 .sequenceNumber{fill:white;}#mermaid-svg-2BjggreBf9Md0lf4 #sequencenumber{fill:#333;}#mermaid-svg-2BjggreBf9Md0lf4 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-2BjggreBf9Md0lf4 .messageText{fill:#333;stroke:none;}#mermaid-svg-2BjggreBf9Md0lf4 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2BjggreBf9Md0lf4 .labelText,#mermaid-svg-2BjggreBf9Md0lf4 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-2BjggreBf9Md0lf4 .loopText,#mermaid-svg-2BjggreBf9Md0lf4 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-2BjggreBf9Md0lf4 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2BjggreBf9Md0lf4 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-2BjggreBf9Md0lf4 .noteText,#mermaid-svg-2BjggreBf9Md0lf4 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-2BjggreBf9Md0lf4 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2BjggreBf9Md0lf4 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2BjggreBf9Md0lf4 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2BjggreBf9Md0lf4 .actorPopupMenu{position:absolute;}#mermaid-svg-2BjggreBf9Md0lf4 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-2BjggreBf9Md0lf4 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2BjggreBf9Md0lf4 .actor-man circle,#mermaid-svg-2BjggreBf9Md0lf4 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-2BjggreBf9Md0lf4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 之后某处写入 .value 读取 .value记录: C 读过我通知登记簿里的 C调度 C 重组
为什么要"快照"(Snapshot)
如果多个地方同时改状态(多线程、协程),读到的值可能不一致。快照系统提供一致性视图:一次读取看到的是某个时间点的"快照",写操作在提交前对其他快照不可见,提交时原子生效。
#mermaid-svg-DzWCWWRrxU8QDXOL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DzWCWWRrxU8QDXOL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DzWCWWRrxU8QDXOL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DzWCWWRrxU8QDXOL .error-icon{fill:#552222;}#mermaid-svg-DzWCWWRrxU8QDXOL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DzWCWWRrxU8QDXOL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DzWCWWRrxU8QDXOL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DzWCWWRrxU8QDXOL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DzWCWWRrxU8QDXOL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DzWCWWRrxU8QDXOL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DzWCWWRrxU8QDXOL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DzWCWWRrxU8QDXOL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DzWCWWRrxU8QDXOL .marker.cross{stroke:#333333;}#mermaid-svg-DzWCWWRrxU8QDXOL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DzWCWWRrxU8QDXOL p{margin:0;}#mermaid-svg-DzWCWWRrxU8QDXOL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DzWCWWRrxU8QDXOL .cluster-label text{fill:#333;}#mermaid-svg-DzWCWWRrxU8QDXOL .cluster-label span{color:#333;}#mermaid-svg-DzWCWWRrxU8QDXOL .cluster-label span p{background-color:transparent;}#mermaid-svg-DzWCWWRrxU8QDXOL .label text,#mermaid-svg-DzWCWWRrxU8QDXOL span{fill:#333;color:#333;}#mermaid-svg-DzWCWWRrxU8QDXOL .node rect,#mermaid-svg-DzWCWWRrxU8QDXOL .node circle,#mermaid-svg-DzWCWWRrxU8QDXOL .node ellipse,#mermaid-svg-DzWCWWRrxU8QDXOL .node polygon,#mermaid-svg-DzWCWWRrxU8QDXOL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DzWCWWRrxU8QDXOL .rough-node .label text,#mermaid-svg-DzWCWWRrxU8QDXOL .node .label text,#mermaid-svg-DzWCWWRrxU8QDXOL .image-shape .label,#mermaid-svg-DzWCWWRrxU8QDXOL .icon-shape .label{text-anchor:middle;}#mermaid-svg-DzWCWWRrxU8QDXOL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DzWCWWRrxU8QDXOL .rough-node .label,#mermaid-svg-DzWCWWRrxU8QDXOL .node .label,#mermaid-svg-DzWCWWRrxU8QDXOL .image-shape .label,#mermaid-svg-DzWCWWRrxU8QDXOL .icon-shape .label{text-align:center;}#mermaid-svg-DzWCWWRrxU8QDXOL .node.clickable{cursor:pointer;}#mermaid-svg-DzWCWWRrxU8QDXOL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DzWCWWRrxU8QDXOL .arrowheadPath{fill:#333333;}#mermaid-svg-DzWCWWRrxU8QDXOL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DzWCWWRrxU8QDXOL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DzWCWWRrxU8QDXOL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DzWCWWRrxU8QDXOL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DzWCWWRrxU8QDXOL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DzWCWWRrxU8QDXOL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DzWCWWRrxU8QDXOL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DzWCWWRrxU8QDXOL .cluster text{fill:#333;}#mermaid-svg-DzWCWWRrxU8QDXOL .cluster span{color:#333;}#mermaid-svg-DzWCWWRrxU8QDXOL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DzWCWWRrxU8QDXOL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DzWCWWRrxU8QDXOL rect.text{fill:none;stroke-width:0;}#mermaid-svg-DzWCWWRrxU8QDXOL .icon-shape,#mermaid-svg-DzWCWWRrxU8QDXOL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DzWCWWRrxU8QDXOL .icon-shape p,#mermaid-svg-DzWCWWRrxU8QDXOL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DzWCWWRrxU8QDXOL .icon-shape .label rect,#mermaid-svg-DzWCWWRrxU8QDXOL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DzWCWWRrxU8QDXOL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DzWCWWRrxU8QDXOL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DzWCWWRrxU8QDXOL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} apply 提交
看不到未提交的修改
全局状态
快照A: 读到一致视图
快照B: 独立修改
Snapshot.current/Snapshot.takeSnapshot()可手动创建快照。SnapshotStateObserver负责把"读"和"写"关联起来做重组调度。mutableStateOf的写入会走SnapshotMutableStateImpl,在快照中记录修改,apply时通知观察者。
为什么这很重要(面试话术)
- 它解释了"Compose 为什么能精确知道哪个 Composable 要重组"------不是靠 diff 整棵树,而是靠读取追踪。
- 它解释了"为什么状态要在 Composable 里读"------不读就没人登记,写了也不会触发重组(常见 bug:在 Composable 外读 state 传给参数,导致不更新)。
- 它解释了线程安全与一致性。
一句话总结:Compose 用快照系统给 State 做"读取追踪",读过的 Composable 会被登记,写入时精确通知它们重组;快照保证并发读写的一致性视图。
6. 重组作用域与智能重组(Smart Recomposition)
概念
重组作用域:一个 Composable 函数体(更准确说是编译器插入的 restart group)就是一个重组作用域。状态被读取时,登记的是"最近的重组作用域"。
智能重组:重组时,Compose 会尽量跳过输入没变的 Composable。能否跳过取决于两点:
- 该 Composable 是否可跳过(参数稳定 + 编译器判定)。
- 传入参数是否相等 (
equals)。
图示:作用域如何决定重组范围
#mermaid-svg-CrNgO3AUwqJgVEaV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CrNgO3AUwqJgVEaV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CrNgO3AUwqJgVEaV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CrNgO3AUwqJgVEaV .error-icon{fill:#552222;}#mermaid-svg-CrNgO3AUwqJgVEaV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CrNgO3AUwqJgVEaV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CrNgO3AUwqJgVEaV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CrNgO3AUwqJgVEaV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CrNgO3AUwqJgVEaV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CrNgO3AUwqJgVEaV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CrNgO3AUwqJgVEaV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CrNgO3AUwqJgVEaV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CrNgO3AUwqJgVEaV .marker.cross{stroke:#333333;}#mermaid-svg-CrNgO3AUwqJgVEaV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CrNgO3AUwqJgVEaV p{margin:0;}#mermaid-svg-CrNgO3AUwqJgVEaV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CrNgO3AUwqJgVEaV .cluster-label text{fill:#333;}#mermaid-svg-CrNgO3AUwqJgVEaV .cluster-label span{color:#333;}#mermaid-svg-CrNgO3AUwqJgVEaV .cluster-label span p{background-color:transparent;}#mermaid-svg-CrNgO3AUwqJgVEaV .label text,#mermaid-svg-CrNgO3AUwqJgVEaV span{fill:#333;color:#333;}#mermaid-svg-CrNgO3AUwqJgVEaV .node rect,#mermaid-svg-CrNgO3AUwqJgVEaV .node circle,#mermaid-svg-CrNgO3AUwqJgVEaV .node ellipse,#mermaid-svg-CrNgO3AUwqJgVEaV .node polygon,#mermaid-svg-CrNgO3AUwqJgVEaV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CrNgO3AUwqJgVEaV .rough-node .label text,#mermaid-svg-CrNgO3AUwqJgVEaV .node .label text,#mermaid-svg-CrNgO3AUwqJgVEaV .image-shape .label,#mermaid-svg-CrNgO3AUwqJgVEaV .icon-shape .label{text-anchor:middle;}#mermaid-svg-CrNgO3AUwqJgVEaV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CrNgO3AUwqJgVEaV .rough-node .label,#mermaid-svg-CrNgO3AUwqJgVEaV .node .label,#mermaid-svg-CrNgO3AUwqJgVEaV .image-shape .label,#mermaid-svg-CrNgO3AUwqJgVEaV .icon-shape .label{text-align:center;}#mermaid-svg-CrNgO3AUwqJgVEaV .node.clickable{cursor:pointer;}#mermaid-svg-CrNgO3AUwqJgVEaV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CrNgO3AUwqJgVEaV .arrowheadPath{fill:#333333;}#mermaid-svg-CrNgO3AUwqJgVEaV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CrNgO3AUwqJgVEaV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CrNgO3AUwqJgVEaV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CrNgO3AUwqJgVEaV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CrNgO3AUwqJgVEaV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CrNgO3AUwqJgVEaV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CrNgO3AUwqJgVEaV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CrNgO3AUwqJgVEaV .cluster text{fill:#333;}#mermaid-svg-CrNgO3AUwqJgVEaV .cluster span{color:#333;}#mermaid-svg-CrNgO3AUwqJgVEaV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CrNgO3AUwqJgVEaV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CrNgO3AUwqJgVEaV rect.text{fill:none;stroke-width:0;}#mermaid-svg-CrNgO3AUwqJgVEaV .icon-shape,#mermaid-svg-CrNgO3AUwqJgVEaV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CrNgO3AUwqJgVEaV .icon-shape p,#mermaid-svg-CrNgO3AUwqJgVEaV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CrNgO3AUwqJgVEaV .icon-shape .label rect,#mermaid-svg-CrNgO3AUwqJgVEaV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CrNgO3AUwqJgVEaV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CrNgO3AUwqJgVEaV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CrNgO3AUwqJgVEaV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Root
Parent 读取了 stateA
Child1 参数未变 → skip
Child2 参数含 stateA → 重组
注意:如果 stateA 在 Parent 里被读取,那么整个 Parent 函数体 都会重跑(它是最近作用域),Child1 能否被跳过取决于其参数是否稳定且相等。
让重组范围最小化的技巧
- 把状态读取下沉到最小的 Composable:谁用谁读,别在父级读了往下传。
- 用 lambda 延迟读取 :
Modifier.offset { }、graphicsLayer { }。 - 用
derivedStateOf:从多个状态派生出"结果型"状态,避免上游频繁变化导致下游频繁重组。
kotlin
// 反例:在父级读,父级整个重跑
@Composable
fun Parent(state: ScrollState) {
val showButton = state.value > 0 // 父级读取
if (showButton) Button()
}
// 正例:derivedStateOf 让"是否显示"只在布尔翻转时变化
@Composable
fun Parent(state: ScrollState) {
val showButton by remember { derivedStateOf { state.value > 0 } }
if (showButton) Button()
}
derivedStateOf 的意义:scrollState.value 可能每帧都变,但 > 0 的布尔值只在跨越 0 时变一次,下游重组次数大幅减少。
一句话总结:状态在哪个 Composable 被读,哪个作用域就重组;用"读取下沉 + lambda 延迟 + derivedStateOf"把重组范围压到最小。
7. 稳定性(Stability):Stable / Unstable / Immutable
概念
Compose 编译器要判断一个 Composable 能否"跳过"(skip),前提是它的所有参数类型都是稳定的。
| 标记 | 含义 | 例子 |
|---|---|---|
@Immutable |
所有公开属性永不改变,且类型本身稳定 | val 全不可变的 data class、String、基本类型 |
@Stable |
值可能变,但变化会通知 Compose(如 MutableState);或 equals 可靠 |
MutableState、State |
| Unstable | 编译器无法保证,默认不能 skip | 含 List/Set/Map 的 data class、var 属性 |
关键陷阱:List 是 Unstable
kotlin
// Unstable:List 是接口,编译器不知道实现是否可变
data class UiState(val items: List<Item>)
// 方案1:用不可变集合库(kotlinx.collections.immutable)
data class UiState(val items: ImmutableList<Item>)
// 方案2:标注 @Immutable(你保证不修改)
@Immutable
data class UiState(val items: List<Item>)
// 方案3:用 Stable 的包装(如 SnapshotStateList 是 stable 的)
为什么 List 会被判定 Unstable
#mermaid-svg-Dzw4t09kQuRQzIae{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Dzw4t09kQuRQzIae .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Dzw4t09kQuRQzIae .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Dzw4t09kQuRQzIae .error-icon{fill:#552222;}#mermaid-svg-Dzw4t09kQuRQzIae .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Dzw4t09kQuRQzIae .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Dzw4t09kQuRQzIae .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Dzw4t09kQuRQzIae .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Dzw4t09kQuRQzIae .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Dzw4t09kQuRQzIae .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Dzw4t09kQuRQzIae .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Dzw4t09kQuRQzIae .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Dzw4t09kQuRQzIae .marker.cross{stroke:#333333;}#mermaid-svg-Dzw4t09kQuRQzIae svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Dzw4t09kQuRQzIae p{margin:0;}#mermaid-svg-Dzw4t09kQuRQzIae .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Dzw4t09kQuRQzIae .cluster-label text{fill:#333;}#mermaid-svg-Dzw4t09kQuRQzIae .cluster-label span{color:#333;}#mermaid-svg-Dzw4t09kQuRQzIae .cluster-label span p{background-color:transparent;}#mermaid-svg-Dzw4t09kQuRQzIae .label text,#mermaid-svg-Dzw4t09kQuRQzIae span{fill:#333;color:#333;}#mermaid-svg-Dzw4t09kQuRQzIae .node rect,#mermaid-svg-Dzw4t09kQuRQzIae .node circle,#mermaid-svg-Dzw4t09kQuRQzIae .node ellipse,#mermaid-svg-Dzw4t09kQuRQzIae .node polygon,#mermaid-svg-Dzw4t09kQuRQzIae .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Dzw4t09kQuRQzIae .rough-node .label text,#mermaid-svg-Dzw4t09kQuRQzIae .node .label text,#mermaid-svg-Dzw4t09kQuRQzIae .image-shape .label,#mermaid-svg-Dzw4t09kQuRQzIae .icon-shape .label{text-anchor:middle;}#mermaid-svg-Dzw4t09kQuRQzIae .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Dzw4t09kQuRQzIae .rough-node .label,#mermaid-svg-Dzw4t09kQuRQzIae .node .label,#mermaid-svg-Dzw4t09kQuRQzIae .image-shape .label,#mermaid-svg-Dzw4t09kQuRQzIae .icon-shape .label{text-align:center;}#mermaid-svg-Dzw4t09kQuRQzIae .node.clickable{cursor:pointer;}#mermaid-svg-Dzw4t09kQuRQzIae .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Dzw4t09kQuRQzIae .arrowheadPath{fill:#333333;}#mermaid-svg-Dzw4t09kQuRQzIae .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Dzw4t09kQuRQzIae .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Dzw4t09kQuRQzIae .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Dzw4t09kQuRQzIae .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Dzw4t09kQuRQzIae .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Dzw4t09kQuRQzIae .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Dzw4t09kQuRQzIae .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Dzw4t09kQuRQzIae .cluster text{fill:#333;}#mermaid-svg-Dzw4t09kQuRQzIae .cluster span{color:#333;}#mermaid-svg-Dzw4t09kQuRQzIae div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Dzw4t09kQuRQzIae .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Dzw4t09kQuRQzIae rect.text{fill:none;stroke-width:0;}#mermaid-svg-Dzw4t09kQuRQzIae .icon-shape,#mermaid-svg-Dzw4t09kQuRQzIae .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Dzw4t09kQuRQzIae .icon-shape p,#mermaid-svg-Dzw4t09kQuRQzIae .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Dzw4t09kQuRQzIae .icon-shape .label rect,#mermaid-svg-Dzw4t09kQuRQzIae .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Dzw4t09kQuRQzIae .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Dzw4t09kQuRQzIae .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Dzw4t09kQuRQzIae :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} List 接口
ArrayList 可变
LinkedList 可变
ImmutableList 不可变
编译器无法保证 equals 后内容不变
判定 Unstable → 不能 skip
编译器看到 List 只知道是接口,无法保证"两个 equals 的 List 内部内容永远一致",所以保守判为 Unstable。
编译器报告
可以用 Compose Compiler Metrics 查看每个 Composable 是否 skippable、每个类是否 stable:
// build.gradle 开启
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_metrics")
metricsDestination = layout.buildDirectory.dir("compose_metrics")
}
一句话总结 :参数稳定才能 skip;
List等接口类型默认 Unstable,用ImmutableList/@Immutable/@Stable修正。
第三阶段:副作用与生命周期
8. 副作用 API 全家桶
为什么需要副作用
Composable 会被反复、可取消、可并发地执行,所以不能在函数体里直接做副作用(网络请求、注册监听、弹 Toast)。副作用 API 把副作用"挂"到组合生命周期上,保证正确的执行时机和清理。
四件套对比
#mermaid-svg-jj69ONbPg1yV6V9l{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jj69ONbPg1yV6V9l .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jj69ONbPg1yV6V9l .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jj69ONbPg1yV6V9l .error-icon{fill:#552222;}#mermaid-svg-jj69ONbPg1yV6V9l .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jj69ONbPg1yV6V9l .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jj69ONbPg1yV6V9l .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jj69ONbPg1yV6V9l .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jj69ONbPg1yV6V9l .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jj69ONbPg1yV6V9l .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jj69ONbPg1yV6V9l .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jj69ONbPg1yV6V9l .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jj69ONbPg1yV6V9l .marker.cross{stroke:#333333;}#mermaid-svg-jj69ONbPg1yV6V9l svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jj69ONbPg1yV6V9l p{margin:0;}#mermaid-svg-jj69ONbPg1yV6V9l .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jj69ONbPg1yV6V9l .cluster-label text{fill:#333;}#mermaid-svg-jj69ONbPg1yV6V9l .cluster-label span{color:#333;}#mermaid-svg-jj69ONbPg1yV6V9l .cluster-label span p{background-color:transparent;}#mermaid-svg-jj69ONbPg1yV6V9l .label text,#mermaid-svg-jj69ONbPg1yV6V9l span{fill:#333;color:#333;}#mermaid-svg-jj69ONbPg1yV6V9l .node rect,#mermaid-svg-jj69ONbPg1yV6V9l .node circle,#mermaid-svg-jj69ONbPg1yV6V9l .node ellipse,#mermaid-svg-jj69ONbPg1yV6V9l .node polygon,#mermaid-svg-jj69ONbPg1yV6V9l .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jj69ONbPg1yV6V9l .rough-node .label text,#mermaid-svg-jj69ONbPg1yV6V9l .node .label text,#mermaid-svg-jj69ONbPg1yV6V9l .image-shape .label,#mermaid-svg-jj69ONbPg1yV6V9l .icon-shape .label{text-anchor:middle;}#mermaid-svg-jj69ONbPg1yV6V9l .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jj69ONbPg1yV6V9l .rough-node .label,#mermaid-svg-jj69ONbPg1yV6V9l .node .label,#mermaid-svg-jj69ONbPg1yV6V9l .image-shape .label,#mermaid-svg-jj69ONbPg1yV6V9l .icon-shape .label{text-align:center;}#mermaid-svg-jj69ONbPg1yV6V9l .node.clickable{cursor:pointer;}#mermaid-svg-jj69ONbPg1yV6V9l .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jj69ONbPg1yV6V9l .arrowheadPath{fill:#333333;}#mermaid-svg-jj69ONbPg1yV6V9l .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jj69ONbPg1yV6V9l .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jj69ONbPg1yV6V9l .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jj69ONbPg1yV6V9l .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jj69ONbPg1yV6V9l .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jj69ONbPg1yV6V9l .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jj69ONbPg1yV6V9l .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jj69ONbPg1yV6V9l .cluster text{fill:#333;}#mermaid-svg-jj69ONbPg1yV6V9l .cluster span{color:#333;}#mermaid-svg-jj69ONbPg1yV6V9l div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jj69ONbPg1yV6V9l .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jj69ONbPg1yV6V9l rect.text{fill:none;stroke-width:0;}#mermaid-svg-jj69ONbPg1yV6V9l .icon-shape,#mermaid-svg-jj69ONbPg1yV6V9l .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jj69ONbPg1yV6V9l .icon-shape p,#mermaid-svg-jj69ONbPg1yV6V9l .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jj69ONbPg1yV6V9l .icon-shape .label rect,#mermaid-svg-jj69ONbPg1yV6V9l .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jj69ONbPg1yV6V9l .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jj69ONbPg1yV6V9l .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jj69ONbPg1yV6V9l :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 手动控制协程
rememberCoroutineScope: 拿一个与组合绑定的 scope
每次重组后执行
SideEffect: 每次成功重组后执行, 无清理
有 key 且随 key 重启
LaunchedEffect: 启动协程, key 变则取消重启
DisposableEffect: 进入时执行, 离开/ key 变时 onDispose
| API | 时机 | 清理 | 典型用途 |
|---|---|---|---|
LaunchedEffect(key) |
进入组合 / key 变时启动协程 | 离开或 key 变时取消 | 网络请求、动画、订阅 Flow |
DisposableEffect(key) |
进入组合执行 | onDispose 在离开/key 变时执行 |
注册/注销监听、Lifecycle 绑定 |
SideEffect |
每次成功重组后 | 无 | 把 Compose 状态同步给非 Compose 代码 |
rememberCoroutineScope |
组合期间有效 | 离开组合时取消 | 事件回调里启动协程(如 onClick 里 launch) |
举例
kotlin
@Composable
fun Screen(userId: String) {
// userId 变 → 取消旧请求,发新请求
LaunchedEffect(userId) {
val user = repo.load(userId)
// 更新状态...
}
// 注册监听,离开时注销
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event -> ... }
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
val scope = rememberCoroutineScope()
Button(onClick = { scope.launch { ... } }) { Text("点我") }
}
常见误区
- 在 Composable 函数体直接
launch/ 发请求 → 每次重组都执行,错误。 LaunchedEffect(Unit)表示"只随进入组合执行一次",不是"每次重组"。rememberCoroutineScope的协程在离开组合时被取消,别拿它做需要跨页面存活的任务。
一句话总结 :副作用必须挂到组合生命周期上;
LaunchedEffect管协程、DisposableEffect管注册注销、SideEffect管每次重组后的同步、rememberCoroutineScope管事件回调里的协程。
9. 副作用与组合生命周期(key 的作用)
概念
每个副作用 API 都接受 key(可变参数)。key 是副作用的"身份标识":key 不变则副作用实例复用;key 变则旧实例清理、新实例重建。
#mermaid-svg-G988VbbEOmfk0XtV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-G988VbbEOmfk0XtV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-G988VbbEOmfk0XtV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-G988VbbEOmfk0XtV .error-icon{fill:#552222;}#mermaid-svg-G988VbbEOmfk0XtV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-G988VbbEOmfk0XtV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-G988VbbEOmfk0XtV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-G988VbbEOmfk0XtV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-G988VbbEOmfk0XtV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-G988VbbEOmfk0XtV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-G988VbbEOmfk0XtV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-G988VbbEOmfk0XtV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-G988VbbEOmfk0XtV .marker.cross{stroke:#333333;}#mermaid-svg-G988VbbEOmfk0XtV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-G988VbbEOmfk0XtV p{margin:0;}#mermaid-svg-G988VbbEOmfk0XtV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-G988VbbEOmfk0XtV .cluster-label text{fill:#333;}#mermaid-svg-G988VbbEOmfk0XtV .cluster-label span{color:#333;}#mermaid-svg-G988VbbEOmfk0XtV .cluster-label span p{background-color:transparent;}#mermaid-svg-G988VbbEOmfk0XtV .label text,#mermaid-svg-G988VbbEOmfk0XtV span{fill:#333;color:#333;}#mermaid-svg-G988VbbEOmfk0XtV .node rect,#mermaid-svg-G988VbbEOmfk0XtV .node circle,#mermaid-svg-G988VbbEOmfk0XtV .node ellipse,#mermaid-svg-G988VbbEOmfk0XtV .node polygon,#mermaid-svg-G988VbbEOmfk0XtV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-G988VbbEOmfk0XtV .rough-node .label text,#mermaid-svg-G988VbbEOmfk0XtV .node .label text,#mermaid-svg-G988VbbEOmfk0XtV .image-shape .label,#mermaid-svg-G988VbbEOmfk0XtV .icon-shape .label{text-anchor:middle;}#mermaid-svg-G988VbbEOmfk0XtV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-G988VbbEOmfk0XtV .rough-node .label,#mermaid-svg-G988VbbEOmfk0XtV .node .label,#mermaid-svg-G988VbbEOmfk0XtV .image-shape .label,#mermaid-svg-G988VbbEOmfk0XtV .icon-shape .label{text-align:center;}#mermaid-svg-G988VbbEOmfk0XtV .node.clickable{cursor:pointer;}#mermaid-svg-G988VbbEOmfk0XtV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-G988VbbEOmfk0XtV .arrowheadPath{fill:#333333;}#mermaid-svg-G988VbbEOmfk0XtV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-G988VbbEOmfk0XtV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-G988VbbEOmfk0XtV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G988VbbEOmfk0XtV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-G988VbbEOmfk0XtV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G988VbbEOmfk0XtV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-G988VbbEOmfk0XtV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-G988VbbEOmfk0XtV .cluster text{fill:#333;}#mermaid-svg-G988VbbEOmfk0XtV .cluster span{color:#333;}#mermaid-svg-G988VbbEOmfk0XtV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-G988VbbEOmfk0XtV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-G988VbbEOmfk0XtV rect.text{fill:none;stroke-width:0;}#mermaid-svg-G988VbbEOmfk0XtV .icon-shape,#mermaid-svg-G988VbbEOmfk0XtV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G988VbbEOmfk0XtV .icon-shape p,#mermaid-svg-G988VbbEOmfk0XtV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-G988VbbEOmfk0XtV .icon-shape .label rect,#mermaid-svg-G988VbbEOmfk0XtV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G988VbbEOmfk0XtV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-G988VbbEOmfk0XtV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-G988VbbEOmfk0XtV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} key 变为 2
key=1
副作用实例A
onDispose/取消 A
新建副作用实例B
图示:LaunchedEffect 的 key 语义
kotlin
LaunchedEffect(userId) { load(userId) }
// userId=1 进入 → 启动协程A
// userId=2 → 取消协程A,启动协程B
// 离开组合 → 取消当前协程
DisposableEffect 的对称性
kotlin
DisposableEffect(unit) {
register(unit)
onDispose { unregister(unit) } // 必须成对
}
onDispose 在两种时机执行:key 变化前、离开组合时。这保证了"注册/注销"成对,避免泄漏。
与 remember 的关系
remember(key)也是同样的身份语义:key 变则重新计算。- 副作用 API 内部其实也依赖
remember来持有副作用实例。
一句话总结 :key 是副作用的身份,key 不变则复用、key 变则清理重建;
DisposableEffect靠这个保证注册/注销成对。
第四阶段:布局与绘制
10. 布局原理:Constraints / MeasurePolicy
概念
Compose 布局是"约束自上而下、尺寸自下而上"的单遍测量:
#mermaid-svg-qhlnwapJqzwdRidr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qhlnwapJqzwdRidr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qhlnwapJqzwdRidr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qhlnwapJqzwdRidr .error-icon{fill:#552222;}#mermaid-svg-qhlnwapJqzwdRidr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qhlnwapJqzwdRidr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qhlnwapJqzwdRidr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qhlnwapJqzwdRidr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qhlnwapJqzwdRidr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qhlnwapJqzwdRidr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qhlnwapJqzwdRidr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qhlnwapJqzwdRidr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qhlnwapJqzwdRidr .marker.cross{stroke:#333333;}#mermaid-svg-qhlnwapJqzwdRidr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qhlnwapJqzwdRidr p{margin:0;}#mermaid-svg-qhlnwapJqzwdRidr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qhlnwapJqzwdRidr .cluster-label text{fill:#333;}#mermaid-svg-qhlnwapJqzwdRidr .cluster-label span{color:#333;}#mermaid-svg-qhlnwapJqzwdRidr .cluster-label span p{background-color:transparent;}#mermaid-svg-qhlnwapJqzwdRidr .label text,#mermaid-svg-qhlnwapJqzwdRidr span{fill:#333;color:#333;}#mermaid-svg-qhlnwapJqzwdRidr .node rect,#mermaid-svg-qhlnwapJqzwdRidr .node circle,#mermaid-svg-qhlnwapJqzwdRidr .node ellipse,#mermaid-svg-qhlnwapJqzwdRidr .node polygon,#mermaid-svg-qhlnwapJqzwdRidr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qhlnwapJqzwdRidr .rough-node .label text,#mermaid-svg-qhlnwapJqzwdRidr .node .label text,#mermaid-svg-qhlnwapJqzwdRidr .image-shape .label,#mermaid-svg-qhlnwapJqzwdRidr .icon-shape .label{text-anchor:middle;}#mermaid-svg-qhlnwapJqzwdRidr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qhlnwapJqzwdRidr .rough-node .label,#mermaid-svg-qhlnwapJqzwdRidr .node .label,#mermaid-svg-qhlnwapJqzwdRidr .image-shape .label,#mermaid-svg-qhlnwapJqzwdRidr .icon-shape .label{text-align:center;}#mermaid-svg-qhlnwapJqzwdRidr .node.clickable{cursor:pointer;}#mermaid-svg-qhlnwapJqzwdRidr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qhlnwapJqzwdRidr .arrowheadPath{fill:#333333;}#mermaid-svg-qhlnwapJqzwdRidr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qhlnwapJqzwdRidr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qhlnwapJqzwdRidr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qhlnwapJqzwdRidr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qhlnwapJqzwdRidr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qhlnwapJqzwdRidr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qhlnwapJqzwdRidr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qhlnwapJqzwdRidr .cluster text{fill:#333;}#mermaid-svg-qhlnwapJqzwdRidr .cluster span{color:#333;}#mermaid-svg-qhlnwapJqzwdRidr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qhlnwapJqzwdRidr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qhlnwapJqzwdRidr rect.text{fill:none;stroke-width:0;}#mermaid-svg-qhlnwapJqzwdRidr .icon-shape,#mermaid-svg-qhlnwapJqzwdRidr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qhlnwapJqzwdRidr .icon-shape p,#mermaid-svg-qhlnwapJqzwdRidr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qhlnwapJqzwdRidr .icon-shape .label rect,#mermaid-svg-qhlnwapJqzwdRidr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qhlnwapJqzwdRidr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qhlnwapJqzwdRidr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qhlnwapJqzwdRidr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 传入 Constraints min/max 宽高
返回自身尺寸
根据子尺寸决定摆放位置
父节点
子节点
Constraints:minWidth/maxWidth/minHeight/maxHeight,父给子的测量约束。- 每个节点通过
MeasurePolicy决定:怎么测量子节点、自己多大、子节点放哪。 - 默认只测量一遍(不像 View 可能 measure 两次),所以布局要高效。
举例:自定义 MeasurePolicy
kotlin
Layout(
content = { /* 子 Composable */ },
measurePolicy = { 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.place(0, y); y += it.height }
}
}
)
关键点(面试常问)
- 单遍测量 :Compose 默认不允许多次测量(
SubcomposeLayout例外,见第 12 节)。 layout(width, height) { ... }:声明自身尺寸并放置子节点。Modifier.layout可以在不写完整 Layout 的情况下微调测量/放置。
一句话总结:父传 Constraints 给子,子返回尺寸,父决定摆放;默认单遍测量,MeasurePolicy 是布局逻辑的核心。
11. Modifier 的本质与顺序敏感性
概念
Modifier 是一个不可变的链式结构 (本质是 Modifier.Node 链表,旧版是 Modifier 的 then 组合)。每个 Modifier 元素会包裹/修饰它后面的内容。
顺序为什么敏感
#mermaid-svg-qWuPM7shU1WSKjzn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qWuPM7shU1WSKjzn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qWuPM7shU1WSKjzn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qWuPM7shU1WSKjzn .error-icon{fill:#552222;}#mermaid-svg-qWuPM7shU1WSKjzn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qWuPM7shU1WSKjzn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qWuPM7shU1WSKjzn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qWuPM7shU1WSKjzn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qWuPM7shU1WSKjzn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qWuPM7shU1WSKjzn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qWuPM7shU1WSKjzn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qWuPM7shU1WSKjzn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qWuPM7shU1WSKjzn .marker.cross{stroke:#333333;}#mermaid-svg-qWuPM7shU1WSKjzn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qWuPM7shU1WSKjzn p{margin:0;}#mermaid-svg-qWuPM7shU1WSKjzn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qWuPM7shU1WSKjzn .cluster-label text{fill:#333;}#mermaid-svg-qWuPM7shU1WSKjzn .cluster-label span{color:#333;}#mermaid-svg-qWuPM7shU1WSKjzn .cluster-label span p{background-color:transparent;}#mermaid-svg-qWuPM7shU1WSKjzn .label text,#mermaid-svg-qWuPM7shU1WSKjzn span{fill:#333;color:#333;}#mermaid-svg-qWuPM7shU1WSKjzn .node rect,#mermaid-svg-qWuPM7shU1WSKjzn .node circle,#mermaid-svg-qWuPM7shU1WSKjzn .node ellipse,#mermaid-svg-qWuPM7shU1WSKjzn .node polygon,#mermaid-svg-qWuPM7shU1WSKjzn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qWuPM7shU1WSKjzn .rough-node .label text,#mermaid-svg-qWuPM7shU1WSKjzn .node .label text,#mermaid-svg-qWuPM7shU1WSKjzn .image-shape .label,#mermaid-svg-qWuPM7shU1WSKjzn .icon-shape .label{text-anchor:middle;}#mermaid-svg-qWuPM7shU1WSKjzn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qWuPM7shU1WSKjzn .rough-node .label,#mermaid-svg-qWuPM7shU1WSKjzn .node .label,#mermaid-svg-qWuPM7shU1WSKjzn .image-shape .label,#mermaid-svg-qWuPM7shU1WSKjzn .icon-shape .label{text-align:center;}#mermaid-svg-qWuPM7shU1WSKjzn .node.clickable{cursor:pointer;}#mermaid-svg-qWuPM7shU1WSKjzn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qWuPM7shU1WSKjzn .arrowheadPath{fill:#333333;}#mermaid-svg-qWuPM7shU1WSKjzn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qWuPM7shU1WSKjzn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qWuPM7shU1WSKjzn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qWuPM7shU1WSKjzn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qWuPM7shU1WSKjzn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qWuPM7shU1WSKjzn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qWuPM7shU1WSKjzn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qWuPM7shU1WSKjzn .cluster text{fill:#333;}#mermaid-svg-qWuPM7shU1WSKjzn .cluster span{color:#333;}#mermaid-svg-qWuPM7shU1WSKjzn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qWuPM7shU1WSKjzn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qWuPM7shU1WSKjzn rect.text{fill:none;stroke-width:0;}#mermaid-svg-qWuPM7shU1WSKjzn .icon-shape,#mermaid-svg-qWuPM7shU1WSKjzn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qWuPM7shU1WSKjzn .icon-shape p,#mermaid-svg-qWuPM7shU1WSKjzn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qWuPM7shU1WSKjzn .icon-shape .label rect,#mermaid-svg-qWuPM7shU1WSKjzn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qWuPM7shU1WSKjzn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qWuPM7shU1WSKjzn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qWuPM7shU1WSKjzn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 顺序B
background
padding 16
内容
先 background 再 padding: 背景包含 padding 区域
顺序A
padding 16
background
内容
先 padding 再 background: 背景只包内容, padding 区域在外
kotlin
// 背景只覆盖内容区
Modifier.padding(16.dp).background(Color.Red)
// 背景覆盖内容 + padding 区
Modifier.background(Color.Red).padding(16.dp)
链式调用中,越靠前的 Modifier 越"外层" (先应用,包裹后面的)。padding 在 background 前 → padding 先改变测量约束,background 只画在 padding 内的内容上。
底层:Modifier.Node
现代 Compose 里 Modifier 由 Modifier.Node 组成,每个 Node 有生命周期(onAttach/onDetach),支持高效复用,避免每次重组重建 Modifier 链。
一句话总结 :Modifier 是链式、不可变、顺序敏感的;越靠前越外层,先应用的会包裹后面的,所以
padding和background的顺序会改变视觉效果。
12. 自定义 Layout 与 SubcomposeLayout
概念
Layout:标准自定义布局,子节点在测量前已确定(content lambda 里的 Composable 会被组合)。SubcomposeLayout:允许根据测量结果再决定组合哪些子节点 (先测量,再按需 subcompose)。典型用例:LazyColumn(先知道可视区域,再只组合可见项)。
#mermaid-svg-7IQME0hXxtqhJouv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7IQME0hXxtqhJouv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7IQME0hXxtqhJouv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7IQME0hXxtqhJouv .error-icon{fill:#552222;}#mermaid-svg-7IQME0hXxtqhJouv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7IQME0hXxtqhJouv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7IQME0hXxtqhJouv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7IQME0hXxtqhJouv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7IQME0hXxtqhJouv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7IQME0hXxtqhJouv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7IQME0hXxtqhJouv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7IQME0hXxtqhJouv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7IQME0hXxtqhJouv .marker.cross{stroke:#333333;}#mermaid-svg-7IQME0hXxtqhJouv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7IQME0hXxtqhJouv p{margin:0;}#mermaid-svg-7IQME0hXxtqhJouv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7IQME0hXxtqhJouv .cluster-label text{fill:#333;}#mermaid-svg-7IQME0hXxtqhJouv .cluster-label span{color:#333;}#mermaid-svg-7IQME0hXxtqhJouv .cluster-label span p{background-color:transparent;}#mermaid-svg-7IQME0hXxtqhJouv .label text,#mermaid-svg-7IQME0hXxtqhJouv span{fill:#333;color:#333;}#mermaid-svg-7IQME0hXxtqhJouv .node rect,#mermaid-svg-7IQME0hXxtqhJouv .node circle,#mermaid-svg-7IQME0hXxtqhJouv .node ellipse,#mermaid-svg-7IQME0hXxtqhJouv .node polygon,#mermaid-svg-7IQME0hXxtqhJouv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7IQME0hXxtqhJouv .rough-node .label text,#mermaid-svg-7IQME0hXxtqhJouv .node .label text,#mermaid-svg-7IQME0hXxtqhJouv .image-shape .label,#mermaid-svg-7IQME0hXxtqhJouv .icon-shape .label{text-anchor:middle;}#mermaid-svg-7IQME0hXxtqhJouv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7IQME0hXxtqhJouv .rough-node .label,#mermaid-svg-7IQME0hXxtqhJouv .node .label,#mermaid-svg-7IQME0hXxtqhJouv .image-shape .label,#mermaid-svg-7IQME0hXxtqhJouv .icon-shape .label{text-align:center;}#mermaid-svg-7IQME0hXxtqhJouv .node.clickable{cursor:pointer;}#mermaid-svg-7IQME0hXxtqhJouv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7IQME0hXxtqhJouv .arrowheadPath{fill:#333333;}#mermaid-svg-7IQME0hXxtqhJouv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7IQME0hXxtqhJouv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7IQME0hXxtqhJouv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7IQME0hXxtqhJouv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7IQME0hXxtqhJouv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7IQME0hXxtqhJouv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7IQME0hXxtqhJouv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7IQME0hXxtqhJouv .cluster text{fill:#333;}#mermaid-svg-7IQME0hXxtqhJouv .cluster span{color:#333;}#mermaid-svg-7IQME0hXxtqhJouv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7IQME0hXxtqhJouv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7IQME0hXxtqhJouv rect.text{fill:none;stroke-width:0;}#mermaid-svg-7IQME0hXxtqhJouv .icon-shape,#mermaid-svg-7IQME0hXxtqhJouv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7IQME0hXxtqhJouv .icon-shape p,#mermaid-svg-7IQME0hXxtqhJouv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7IQME0hXxtqhJouv .icon-shape .label rect,#mermaid-svg-7IQME0hXxtqhJouv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7IQME0hXxtqhJouv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7IQME0hXxtqhJouv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7IQME0hXxtqhJouv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Layout
先组合全部子节点
再测量
SubcomposeLayout
先测量/拿到约束
按需 subcompose 子节点
再测量这些子节点
为什么 LazyColumn 必须用 SubcomposeLayout
惰性列表不可能预先组合所有 item,必须先知道视口大小和滚动位置,才能决定组合哪几项。这就是 SubcomposeLayout 的用武之地------它打破了"先组合后测量"的顺序。
代价
SubcomposeLayout 的 subcompose 发生在测量阶段,更昂贵,且可能引入多次测量,非必要不用。
一句话总结 :
Layout先组合后测量;SubcomposeLayout允许测量阶段按需组合子节点(LazyColumn 的基础),但更贵,慎用。
第五阶段:性能与底层
13. Slot Table 与 Composition 数据结构
概念
Compose 运行时维护一棵Slot Table(槽位表),记录:
- 每个 Composable 的"槽位"(存
remember的值、状态等)。 - 树结构(group 的嵌套关系)。
- 重组时需要的信息(哪些 group 可跳过、参数是否变化)。
#mermaid-svg-urt2X0M9tCLdDHBU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-urt2X0M9tCLdDHBU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-urt2X0M9tCLdDHBU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-urt2X0M9tCLdDHBU .error-icon{fill:#552222;}#mermaid-svg-urt2X0M9tCLdDHBU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-urt2X0M9tCLdDHBU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-urt2X0M9tCLdDHBU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-urt2X0M9tCLdDHBU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-urt2X0M9tCLdDHBU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-urt2X0M9tCLdDHBU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-urt2X0M9tCLdDHBU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-urt2X0M9tCLdDHBU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-urt2X0M9tCLdDHBU .marker.cross{stroke:#333333;}#mermaid-svg-urt2X0M9tCLdDHBU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-urt2X0M9tCLdDHBU p{margin:0;}#mermaid-svg-urt2X0M9tCLdDHBU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-urt2X0M9tCLdDHBU .cluster-label text{fill:#333;}#mermaid-svg-urt2X0M9tCLdDHBU .cluster-label span{color:#333;}#mermaid-svg-urt2X0M9tCLdDHBU .cluster-label span p{background-color:transparent;}#mermaid-svg-urt2X0M9tCLdDHBU .label text,#mermaid-svg-urt2X0M9tCLdDHBU span{fill:#333;color:#333;}#mermaid-svg-urt2X0M9tCLdDHBU .node rect,#mermaid-svg-urt2X0M9tCLdDHBU .node circle,#mermaid-svg-urt2X0M9tCLdDHBU .node ellipse,#mermaid-svg-urt2X0M9tCLdDHBU .node polygon,#mermaid-svg-urt2X0M9tCLdDHBU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-urt2X0M9tCLdDHBU .rough-node .label text,#mermaid-svg-urt2X0M9tCLdDHBU .node .label text,#mermaid-svg-urt2X0M9tCLdDHBU .image-shape .label,#mermaid-svg-urt2X0M9tCLdDHBU .icon-shape .label{text-anchor:middle;}#mermaid-svg-urt2X0M9tCLdDHBU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-urt2X0M9tCLdDHBU .rough-node .label,#mermaid-svg-urt2X0M9tCLdDHBU .node .label,#mermaid-svg-urt2X0M9tCLdDHBU .image-shape .label,#mermaid-svg-urt2X0M9tCLdDHBU .icon-shape .label{text-align:center;}#mermaid-svg-urt2X0M9tCLdDHBU .node.clickable{cursor:pointer;}#mermaid-svg-urt2X0M9tCLdDHBU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-urt2X0M9tCLdDHBU .arrowheadPath{fill:#333333;}#mermaid-svg-urt2X0M9tCLdDHBU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-urt2X0M9tCLdDHBU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-urt2X0M9tCLdDHBU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-urt2X0M9tCLdDHBU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-urt2X0M9tCLdDHBU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-urt2X0M9tCLdDHBU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-urt2X0M9tCLdDHBU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-urt2X0M9tCLdDHBU .cluster text{fill:#333;}#mermaid-svg-urt2X0M9tCLdDHBU .cluster span{color:#333;}#mermaid-svg-urt2X0M9tCLdDHBU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-urt2X0M9tCLdDHBU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-urt2X0M9tCLdDHBU rect.text{fill:none;stroke-width:0;}#mermaid-svg-urt2X0M9tCLdDHBU .icon-shape,#mermaid-svg-urt2X0M9tCLdDHBU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-urt2X0M9tCLdDHBU .icon-shape p,#mermaid-svg-urt2X0M9tCLdDHBU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-urt2X0M9tCLdDHBU .icon-shape .label rect,#mermaid-svg-urt2X0M9tCLdDHBU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-urt2X0M9tCLdDHBU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-urt2X0M9tCLdDHBU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-urt2X0M9tCLdDHBU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Root Group
Group: Parent
Group: Child1
Group: Child2
Slot: remember 的值
Slot: remember 的值
关键机制
- Group:编译器为每个 Composable/控制流插入 group,用于定位和跳过。
changed检查:编译器在 group 里插入"参数是否变化"的判断,没变则跳过整个 group。$composer:编译器给每个 Composable 加隐式参数Composer,负责读写 Slot Table、管理 group。- 可移动 group :配合
key让列表项在增删时正确复用状态。
举例(编译器改写示意)
kotlin
// 你写的
@Composable
fun Greeting(name: String) {
Text("Hello $name")
}
// 编译器大致改写为(简化)
fun Greeting(name: String, $composer: Composer, $changed: Int) {
$composer.startRestartGroup(...)
if ($changed and 0b1 != 0 || !$composer.skipping) {
Text("Hello $name", ...)
}
$composer.endRestartGroup()?.updateScope { ... }
}
一句话总结:Slot Table 是 Compose 运行时的"记忆结构",group 定位子树、slot 存 remember 值、changed 检查决定跳过、Composer 负责读写。
14. 重组优化手段
汇总前面散落的优化点,形成清单(面试可直接背):
#mermaid-svg-Qx1Mt7duui4vS20m{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Qx1Mt7duui4vS20m .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Qx1Mt7duui4vS20m .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Qx1Mt7duui4vS20m .error-icon{fill:#552222;}#mermaid-svg-Qx1Mt7duui4vS20m .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Qx1Mt7duui4vS20m .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Qx1Mt7duui4vS20m .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Qx1Mt7duui4vS20m .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Qx1Mt7duui4vS20m .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Qx1Mt7duui4vS20m .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Qx1Mt7duui4vS20m .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Qx1Mt7duui4vS20m .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Qx1Mt7duui4vS20m .marker.cross{stroke:#333333;}#mermaid-svg-Qx1Mt7duui4vS20m svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Qx1Mt7duui4vS20m p{margin:0;}#mermaid-svg-Qx1Mt7duui4vS20m .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Qx1Mt7duui4vS20m .cluster-label text{fill:#333;}#mermaid-svg-Qx1Mt7duui4vS20m .cluster-label span{color:#333;}#mermaid-svg-Qx1Mt7duui4vS20m .cluster-label span p{background-color:transparent;}#mermaid-svg-Qx1Mt7duui4vS20m .label text,#mermaid-svg-Qx1Mt7duui4vS20m span{fill:#333;color:#333;}#mermaid-svg-Qx1Mt7duui4vS20m .node rect,#mermaid-svg-Qx1Mt7duui4vS20m .node circle,#mermaid-svg-Qx1Mt7duui4vS20m .node ellipse,#mermaid-svg-Qx1Mt7duui4vS20m .node polygon,#mermaid-svg-Qx1Mt7duui4vS20m .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Qx1Mt7duui4vS20m .rough-node .label text,#mermaid-svg-Qx1Mt7duui4vS20m .node .label text,#mermaid-svg-Qx1Mt7duui4vS20m .image-shape .label,#mermaid-svg-Qx1Mt7duui4vS20m .icon-shape .label{text-anchor:middle;}#mermaid-svg-Qx1Mt7duui4vS20m .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Qx1Mt7duui4vS20m .rough-node .label,#mermaid-svg-Qx1Mt7duui4vS20m .node .label,#mermaid-svg-Qx1Mt7duui4vS20m .image-shape .label,#mermaid-svg-Qx1Mt7duui4vS20m .icon-shape .label{text-align:center;}#mermaid-svg-Qx1Mt7duui4vS20m .node.clickable{cursor:pointer;}#mermaid-svg-Qx1Mt7duui4vS20m .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Qx1Mt7duui4vS20m .arrowheadPath{fill:#333333;}#mermaid-svg-Qx1Mt7duui4vS20m .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Qx1Mt7duui4vS20m .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Qx1Mt7duui4vS20m .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Qx1Mt7duui4vS20m .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Qx1Mt7duui4vS20m .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Qx1Mt7duui4vS20m .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Qx1Mt7duui4vS20m .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Qx1Mt7duui4vS20m .cluster text{fill:#333;}#mermaid-svg-Qx1Mt7duui4vS20m .cluster span{color:#333;}#mermaid-svg-Qx1Mt7duui4vS20m div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Qx1Mt7duui4vS20m .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Qx1Mt7duui4vS20m rect.text{fill:none;stroke-width:0;}#mermaid-svg-Qx1Mt7duui4vS20m .icon-shape,#mermaid-svg-Qx1Mt7duui4vS20m .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Qx1Mt7duui4vS20m .icon-shape p,#mermaid-svg-Qx1Mt7duui4vS20m .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Qx1Mt7duui4vS20m .icon-shape .label rect,#mermaid-svg-Qx1Mt7duui4vS20m .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Qx1Mt7duui4vS20m .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Qx1Mt7duui4vS20m .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Qx1Mt7duui4vS20m :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 重组优化
读取下沉: 谁用谁读
lambda 延迟读取: offset/graphicsLayer
derivedStateOf: 结果型状态
稳定性: ImmutableList/@Immutable/@Stable
key: 列表项正确复用
避免在参数里传 lambda 导致不稳定: 用 remember 或方法引用
Compose Compiler Metrics 检查 skippable
逐条:
- 读取下沉:状态在最小 Composable 读取,缩小重组作用域。
- lambda 版 Modifier :
offset { }、graphicsLayer { }把读取推迟到 Layout/Draw。 derivedStateOf:把高频变化转成低频"结果"。- 稳定性修正 :
ImmutableList、@Immutable、@Stable。 key:items(list, key = { it.id })保证增删时状态正确复用。- lambda 稳定性 :Composable 参数里的 lambda 若捕获不稳定变量会被判 Unstable,可用
remember包裹或改用方法引用。 - Metrics 报告:用编译器报告定位不可 skip 的 Composable。
一句话总结:优化重组 = 缩小读取作用域 + 延迟读取到后阶段 + 稳定参数类型 + 正确用 key,并用 Metrics 验证。
15. Compose 编译器插件做了什么
概念
@Composable 不是普通注解,Compose 编译器插件会在编译期改写函数:
- 加隐式
Composer参数和$changed参数。 - 插入
startRestartGroup/endRestartGroup管理 group。 - 插入
changed检查,决定是否 skip。 - 对 lambda 做记忆化(
remember化)处理,提升稳定性。 - 校验
@Composable调用规则(只能在 Composable 里调 Composable)。
#mermaid-svg-kwxsRrOyhihPLrh5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kwxsRrOyhihPLrh5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kwxsRrOyhihPLrh5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kwxsRrOyhihPLrh5 .error-icon{fill:#552222;}#mermaid-svg-kwxsRrOyhihPLrh5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kwxsRrOyhihPLrh5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kwxsRrOyhihPLrh5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kwxsRrOyhihPLrh5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kwxsRrOyhihPLrh5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kwxsRrOyhihPLrh5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kwxsRrOyhihPLrh5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kwxsRrOyhihPLrh5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kwxsRrOyhihPLrh5 .marker.cross{stroke:#333333;}#mermaid-svg-kwxsRrOyhihPLrh5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kwxsRrOyhihPLrh5 p{margin:0;}#mermaid-svg-kwxsRrOyhihPLrh5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kwxsRrOyhihPLrh5 .cluster-label text{fill:#333;}#mermaid-svg-kwxsRrOyhihPLrh5 .cluster-label span{color:#333;}#mermaid-svg-kwxsRrOyhihPLrh5 .cluster-label span p{background-color:transparent;}#mermaid-svg-kwxsRrOyhihPLrh5 .label text,#mermaid-svg-kwxsRrOyhihPLrh5 span{fill:#333;color:#333;}#mermaid-svg-kwxsRrOyhihPLrh5 .node rect,#mermaid-svg-kwxsRrOyhihPLrh5 .node circle,#mermaid-svg-kwxsRrOyhihPLrh5 .node ellipse,#mermaid-svg-kwxsRrOyhihPLrh5 .node polygon,#mermaid-svg-kwxsRrOyhihPLrh5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kwxsRrOyhihPLrh5 .rough-node .label text,#mermaid-svg-kwxsRrOyhihPLrh5 .node .label text,#mermaid-svg-kwxsRrOyhihPLrh5 .image-shape .label,#mermaid-svg-kwxsRrOyhihPLrh5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-kwxsRrOyhihPLrh5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kwxsRrOyhihPLrh5 .rough-node .label,#mermaid-svg-kwxsRrOyhihPLrh5 .node .label,#mermaid-svg-kwxsRrOyhihPLrh5 .image-shape .label,#mermaid-svg-kwxsRrOyhihPLrh5 .icon-shape .label{text-align:center;}#mermaid-svg-kwxsRrOyhihPLrh5 .node.clickable{cursor:pointer;}#mermaid-svg-kwxsRrOyhihPLrh5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kwxsRrOyhihPLrh5 .arrowheadPath{fill:#333333;}#mermaid-svg-kwxsRrOyhihPLrh5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kwxsRrOyhihPLrh5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kwxsRrOyhihPLrh5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kwxsRrOyhihPLrh5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kwxsRrOyhihPLrh5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kwxsRrOyhihPLrh5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kwxsRrOyhihPLrh5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kwxsRrOyhihPLrh5 .cluster text{fill:#333;}#mermaid-svg-kwxsRrOyhihPLrh5 .cluster span{color:#333;}#mermaid-svg-kwxsRrOyhihPLrh5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kwxsRrOyhihPLrh5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kwxsRrOyhihPLrh5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-kwxsRrOyhihPLrh5 .icon-shape,#mermaid-svg-kwxsRrOyhihPLrh5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kwxsRrOyhihPLrh5 .icon-shape p,#mermaid-svg-kwxsRrOyhihPLrh5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kwxsRrOyhihPLrh5 .icon-shape .label rect,#mermaid-svg-kwxsRrOyhihPLrh5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kwxsRrOyhihPLrh5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kwxsRrOyhihPLrh5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kwxsRrOyhihPLrh5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 源码 @Composable
Compose 编译器插件
改写: Composer 参数 + group + changed 检查
字节码
为什么重要(面试话术)
- 解释了"Composable 函数为什么没有返回值却能产生 UI"------它通过
Composer往 Slot Table 写节点。 - 解释了"为什么 Composable 不能随便在普通函数里调用"------普通函数没有
Composer。 - 解释了"skip 是怎么实现的"------
changed检查 + group。
一句话总结 :Compose 编译器插件把
@Composable改写成带Composer、group、changed 检查的函数,skip、记忆化、调用约束都由它实现。
第六阶段:工程实践
16. 与 View 互操作
- Compose 里用 View :
AndroidView(factory = { ... }, update = { ... })。 - View 里用 Compose :
ComposeView或setContent { }。 - 注意:
AndroidView的update每次重组会调用,别做重活;factory只创建一次。
一句话总结 :
AndroidView嵌入 View、ComposeView嵌入 Compose,注意 update 的重组频率。
17. 状态提升与单向数据流
- 状态提升(State Hoisting):把状态从子 Composable 提到父级,子变成无状态(stateless)+ 回调。
- 单向数据流(UDF):状态向下流、事件向上流。
#mermaid-svg-xUkpRtiCzulxzoWZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xUkpRtiCzulxzoWZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xUkpRtiCzulxzoWZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xUkpRtiCzulxzoWZ .error-icon{fill:#552222;}#mermaid-svg-xUkpRtiCzulxzoWZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xUkpRtiCzulxzoWZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xUkpRtiCzulxzoWZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xUkpRtiCzulxzoWZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xUkpRtiCzulxzoWZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xUkpRtiCzulxzoWZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xUkpRtiCzulxzoWZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xUkpRtiCzulxzoWZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xUkpRtiCzulxzoWZ .marker.cross{stroke:#333333;}#mermaid-svg-xUkpRtiCzulxzoWZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xUkpRtiCzulxzoWZ p{margin:0;}#mermaid-svg-xUkpRtiCzulxzoWZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xUkpRtiCzulxzoWZ .cluster-label text{fill:#333;}#mermaid-svg-xUkpRtiCzulxzoWZ .cluster-label span{color:#333;}#mermaid-svg-xUkpRtiCzulxzoWZ .cluster-label span p{background-color:transparent;}#mermaid-svg-xUkpRtiCzulxzoWZ .label text,#mermaid-svg-xUkpRtiCzulxzoWZ span{fill:#333;color:#333;}#mermaid-svg-xUkpRtiCzulxzoWZ .node rect,#mermaid-svg-xUkpRtiCzulxzoWZ .node circle,#mermaid-svg-xUkpRtiCzulxzoWZ .node ellipse,#mermaid-svg-xUkpRtiCzulxzoWZ .node polygon,#mermaid-svg-xUkpRtiCzulxzoWZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xUkpRtiCzulxzoWZ .rough-node .label text,#mermaid-svg-xUkpRtiCzulxzoWZ .node .label text,#mermaid-svg-xUkpRtiCzulxzoWZ .image-shape .label,#mermaid-svg-xUkpRtiCzulxzoWZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-xUkpRtiCzulxzoWZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xUkpRtiCzulxzoWZ .rough-node .label,#mermaid-svg-xUkpRtiCzulxzoWZ .node .label,#mermaid-svg-xUkpRtiCzulxzoWZ .image-shape .label,#mermaid-svg-xUkpRtiCzulxzoWZ .icon-shape .label{text-align:center;}#mermaid-svg-xUkpRtiCzulxzoWZ .node.clickable{cursor:pointer;}#mermaid-svg-xUkpRtiCzulxzoWZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xUkpRtiCzulxzoWZ .arrowheadPath{fill:#333333;}#mermaid-svg-xUkpRtiCzulxzoWZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xUkpRtiCzulxzoWZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xUkpRtiCzulxzoWZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xUkpRtiCzulxzoWZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xUkpRtiCzulxzoWZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xUkpRtiCzulxzoWZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xUkpRtiCzulxzoWZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xUkpRtiCzulxzoWZ .cluster text{fill:#333;}#mermaid-svg-xUkpRtiCzulxzoWZ .cluster span{color:#333;}#mermaid-svg-xUkpRtiCzulxzoWZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xUkpRtiCzulxzoWZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xUkpRtiCzulxzoWZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-xUkpRtiCzulxzoWZ .icon-shape,#mermaid-svg-xUkpRtiCzulxzoWZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xUkpRtiCzulxzoWZ .icon-shape p,#mermaid-svg-xUkpRtiCzulxzoWZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xUkpRtiCzulxzoWZ .icon-shape .label rect,#mermaid-svg-xUkpRtiCzulxzoWZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xUkpRtiCzulxzoWZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xUkpRtiCzulxzoWZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xUkpRtiCzulxzoWZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} state 向下
event 向上
ViewModel 持有状态
无状态 Composable
好处:可测试、可复用、状态单一来源。
一句话总结:状态提升 + 单向数据流让 Composable 无状态可复用,状态集中在 ViewModel,事件回调向上。
18. 常见陷阱汇总
| 陷阱 | 后果 | 修正 |
|---|---|---|
| Composable 函数体直接做副作用 | 每次重组重复执行 | 用副作用 API |
| 在父级读状态往下传 | 父级整体重组 | 读取下沉 |
参数含 List |
Unstable 不能 skip | ImmutableList/@Immutable |
remember 忘加 |
状态每次重组丢失 | remember { mutableStateOf } |
列表不用 key |
增删时状态错位 | items(key = { it.id }) |
| 高频状态直接驱动 UI | 每帧重组 | derivedStateOf |
| 在 Composable 外读 state 传参 | 不触发更新 | 在 Composable 内读 |