Compose 知识点

Jetpack Compose 面试知识精读(上半部分:知识结构)

阅读顺序即学习顺序。从"为什么"→"怎么用"→"底层原理"。忘记时从任意一节读起,每节末尾有「一句话总结」。


目录(上半部分)

第一阶段:心智模型

  1. 声明式 UI vs 命令式 UI
  2. Compose 三大阶段:Composition → Layout → Draw
  3. 重组(Recomposition)是什么

第二阶段:状态与重组机制

  1. State / MutableState / remember / rememberSaveable

  2. 快照系统(Snapshot)与状态读取追踪

  3. 重组作用域与智能重组(Smart Recomposition)

  4. 稳定性(Stability):Stable / Unstable / Immutable

第三阶段:副作用与生命周期

  1. 副作用 API 全家桶

  2. 副作用与组合生命周期(key 的作用)

第四阶段:布局与绘制

  1. 布局原理:Constraints / MeasurePolicy

  2. Modifier 的本质与顺序敏感性

  3. 自定义 Layout 与 SubcomposeLayout

第五阶段:性能与底层

  1. Slot Table 与 Composition 数据结构

  2. 重组优化手段

  3. Compose 编译器插件做了什么

第六阶段:工程实践

  1. 与 View 互操作

  2. 状态提升与单向数据流

  3. 常见陷阱汇总


第一阶段:心智模型

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)。

重组的触发条件
  1. 读取的 State 值发生变化(快照系统追踪,见第 5 节)。
  2. 传入的参数发生变化(且参数类型可比较、Composable 可跳过,见第 6、7 节)。
  3. 父级重组且自己无法被跳过。
重组是"可取消、可并发、可能多次"的
  • 重组可能被取消(比如中途状态又变了)。
  • 重组在不同帧可能执行多次。
  • 因此 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。能否跳过取决于两点:

  1. 该 Composable 是否可跳过(参数稳定 + 编译器判定)。
  2. 传入参数是否相等 (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

逐条:

  1. 读取下沉:状态在最小 Composable 读取,缩小重组作用域。
  2. lambda 版 Modifier :offset { }、graphicsLayer { } 把读取推迟到 Layout/Draw。
  3. derivedStateOf:把高频变化转成低频"结果"。
  4. 稳定性修正 :ImmutableList、@Immutable、@Stable。
  5. key :items(list, key = { it.id }) 保证增删时状态正确复用。
  6. lambda 稳定性 :Composable 参数里的 lambda 若捕获不稳定变量会被判 Unstable,可用 remember 包裹或改用方法引用。
  7. Metrics 报告:用编译器报告定位不可 skip 的 Composable。

一句话总结:优化重组 = 缩小读取作用域 + 延迟读取到后阶段 + 稳定参数类型 + 正确用 key,并用 Metrics 验证。


15. Compose 编译器插件做了什么

概念

@Composable 不是普通注解,Compose 编译器插件会在编译期改写函数:

  1. 加隐式 Composer 参数和 $changed 参数。
  2. 插入 startRestartGroup / endRestartGroup 管理 group。
  3. 插入 changed 检查,决定是否 skip。
  4. 对 lambda 做记忆化(remember 化)处理,提升稳定性。
  5. 校验 @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 内读

相关推荐
文章永久免费只为良心1 小时前
Web应用安全与防护_第三章_日志文件包含_从一道 CTFshow 题目讲透「日志投毒型 LFI → RCE」:原理、时序陷阱与全变种攻防手册
android·网络安全·渗透测试·漏洞危害·漏洞描述·日志投毒·红队指南
AIsoft_86891 小时前
学术讲座录音如何整理?录音APP可行吗
android·iphone
Patrick在香港4 小时前
一个斜杠,把毫秒变成十几秒
android·数据库·python·性能优化·正则表达式·回溯·redos
海雅达手持终端PDA5 小时前
工业手持终端的“去手机化”演进:看海雅达如何以“同轴扫摄”终结工程妥协,重塑工业PDA新范式
android·物联网·智能手机·硬件工程·能源·制造·iot
booboosui6 小时前
删掉一个住在本机的 agent:8 处要清,顺序不能反
android
hcgeng6 小时前
Android获取崩溃日志
android
aqi006 小时前
AI编程让鸿蒙开发电子书APP更加快捷
android·华为·harmonyos·鸿蒙·harmony
GY-937 小时前
【Android】Android Studio 中的协程
android