<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Jetpack Compose on zed的博客</title><link>https://www.dust-zed.site/android/compose/</link><description>Recent content in Jetpack Compose on zed的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Fri, 02 Jan 2026 21:30:00 +0800</lastBuildDate><atom:link href="https://www.dust-zed.site/android/compose/index.xml" rel="self" type="application/rss+xml"/><item><title>Jetpack Compose 渲染性能：Modifier、graphicsLayer 与重组边界</title><link>https://www.dust-zed.site/android/compose-performance-rendering/</link><pubDate>Fri, 02 Jan 2026 21:30:00 +0800</pubDate><guid>https://www.dust-zed.site/android/compose-performance-rendering/</guid><description>&lt;p>在开发云游戏客户端的虚拟手柄功能时，我们需要处理高频触摸事件和复杂 UI 动画。这个场景很适合用来理解 Compose 性能：哪些变化会触发布局，哪些变化只影响绘制，哪些状态读取会扩大重组范围。&lt;/p>
&lt;h2 id="核心结论">核心结论
&lt;/h2>&lt;ol>
&lt;li>Modifier 是按顺序包裹的，顺序会影响布局、点击和绘制。&lt;/li>
&lt;li>布局阶段变化通常更贵，能放到绘制或合成阶段的动画尽量下沉。&lt;/li>
&lt;li>&lt;code>graphicsLayer&lt;/code> 适合把平移、缩放、透明度等变换交给 GPU 合成。&lt;/li>
&lt;li>&lt;code>alpha&lt;/code> 可能引入离屏缓冲，要关注裁剪和额外合成成本。&lt;/li>
&lt;li>高频状态要隔离重组范围，不要让整棵 UI 跟着刷新。&lt;/li>
&lt;/ol>
&lt;h2 id="1-modifier-的洋葱模型">1. Modifier 的「洋葱模型」
&lt;/h2>&lt;p>很多初学者容易被 Modifier 的链式调用顺序搞晕。记住一个核心原则：&lt;strong>Modifier 是从外向内包裹的「洋葱」&lt;/strong>。&lt;/p>
&lt;p>考虑以下代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">Box&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">modifier&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">Modifier&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">100.&lt;/span>&lt;span class="n">dp&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">background&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nc">Color&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Red&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">padding&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">10.&lt;/span>&lt;span class="n">dp&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">background&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nc">Color&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Blue&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Compose 并不是简单地按顺序执行属性设置，而是构建了一层层的 &lt;code>LayoutNodeWrapper&lt;/code>：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>最外层&lt;/strong>：&lt;code>size(100.dp)&lt;/code> 限制了整个节点的大小。&lt;/li>
&lt;li>&lt;strong>第二层&lt;/strong>：&lt;code>background(Color.Red)&lt;/code> 在 100x100 的范围内画红底。&lt;/li>
&lt;li>&lt;strong>第三层&lt;/strong>：&lt;code>padding(10.dp)&lt;/code> 将内部的内容向内挤压，现在的可用空间变成了 80x80。&lt;/li>
&lt;li>&lt;strong>最内层&lt;/strong>：&lt;code>background(Color.Blue)&lt;/code> 只在剩下的 80x80 范围内画蓝底。&lt;/li>
&lt;li>&lt;strong>核心&lt;/strong>：&lt;code>Box&lt;/code> 的内容。&lt;/li>
&lt;/ol>
&lt;h3 id="为什么点击区域会错位">为什么点击区域会错位？
&lt;/h3>&lt;p>理解了这个模型，就能解释我在开发虚拟摇杆时遇到的一个诡异 Bug：&lt;strong>&amp;ldquo;点击区域没有跟随滑块移动&amp;rdquo;&lt;/strong>。&lt;/p>
&lt;p>错误代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 错误示范
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">JoystickKnob&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">modifier&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">Modifier&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">clickable&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="cm">/*...*/&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="c1">// 先设置点击
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="n">offset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">10.&lt;/span>&lt;span class="n">dp&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 再移动位置
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在这个&amp;quot;洋葱&amp;quot;中，外层是 &lt;code>clickable&lt;/code>，内层是 &lt;code>offset&lt;/code>。&lt;/p>
&lt;ul>
&lt;li>&lt;code>clickable&lt;/code> 节点拿到了原始位置，它不知道内部的内容被 &lt;code>offset&lt;/code> 移走了。&lt;/li>
&lt;li>&lt;code>offset&lt;/code> 仅仅移动了绘制内容（和布局坐标），但外层的点击检测区域还在原地。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>修正&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 正确示范
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">JoystickKnob&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">modifier&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">Modifier&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">offset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">10.&lt;/span>&lt;span class="n">dp&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 先移动
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="n">clickable&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="cm">/*...*/&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="c1">// 再在外层设置点击
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>现在，&lt;code>offset&lt;/code> 成了外层，它带着内部的 &lt;code>clickable&lt;/code> 区域一起移动了。&lt;/p>
&lt;h2 id="2-alpha-与离屏缓冲">2. Alpha 与离屏缓冲
&lt;/h2>&lt;p>当虚拟摇杆划出底座范围时，我希望它变半透明，于是加了 &lt;code>Modifier.alpha(0.5f)&lt;/code>。结果发现：&lt;strong>超出底座的部分直接被切掉了！&lt;/strong>&lt;/p>
&lt;h3 id="根因分析">根因分析
&lt;/h3>&lt;p>&lt;code>Modifier.alpha&lt;/code> 并不仅仅是改变绘制透明度。为了实现半透明混合，它会强制开启一个 &lt;strong>离屏缓冲 (Off-screen Buffer / Layer)&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>这个 Layer 的大小默认等于组件的大小。&lt;/li>
&lt;li>Layer 自带物理边界（Clipping）。&lt;/li>
&lt;li>当摇杆（内容）通过 &lt;code>offset&lt;/code> 移出这个 Layer 的边界时，超出部分自然就被裁切了。&lt;/li>
&lt;/ul>
&lt;h3 id="优化对策">优化对策
&lt;/h3>&lt;p>对于只是想改变颜色透明度，且内容可能会溢出容器的场景，要谨慎使用 &lt;code>Modifier.alpha&lt;/code>。&lt;/p>
&lt;p>请直接操作颜色的 Alpha 通道：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 性能更好，且不会裁切
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">Box&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">modifier&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nc">Modifier&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">background&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nc">Color&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">Red&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">copy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">alpha&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">0.5f&lt;/span>&lt;span class="p">)))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这直接在当前 Canvas 上绘制半透明颜色，不需要分配额外的 Buffer，既省内存又解决了 Bug。&lt;/p>
&lt;h2 id="3-布局阶段-vs-绘制阶段">3. 布局阶段 vs 绘制阶段
&lt;/h2>&lt;p>虚拟摇杆的位置更新频率极高（60fps+）。如果我们使用 &lt;code>Modifier.offset&lt;/code> 来移动摇杆：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nc">Modifier&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">offset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">dp&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">dp&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里有一个隐患：&lt;code>offset&lt;/code> 会改变组件的 Layout 参数，不仅触发 &lt;strong>Draw (绘制)&lt;/strong> 阶段，还会触发 &lt;strong>Placement (布局)&lt;/strong> 阶段。虽然 Compose 做了优化不会触发 Measure，但在高频场景下，Layout 开销依然可观。&lt;/p>
&lt;h3 id="使用-graphicslayer-降低布局压力">使用 graphicsLayer 降低布局压力
&lt;/h3>&lt;p>更优的方案是使用 &lt;code>Modifier.graphicsLayer&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nc">Modifier&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">graphicsLayer&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">translationX&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">x&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">translationY&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">y&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;strong>跳过 Layout&lt;/strong>：&lt;code>graphicsLayer&lt;/code> 只改变绘制阶段的变换矩阵（Matrix），完全&lt;strong>跳过 Measure 和 Layout 阶段&lt;/strong>。&lt;/li>
&lt;li>&lt;strong>GPU 加速&lt;/strong>：这些变换直接在 GPU 上执行，效率极高。&lt;/li>
&lt;/ul>
&lt;p>这也解释了为什么在 Compose 动画中，优先推荐使用 &lt;code>graphicsLayer&lt;/code> 属性，而不是修改 Layout 参数。&lt;/p>
&lt;h2 id="4-重组边界隔离-provider-pattern">4. 重组边界隔离 (Provider Pattern)
&lt;/h2>&lt;p>全局状态（如 &lt;code>GameUiState&lt;/code>）的变化很容易导致大面积的 &lt;strong>Recomposition (重组)&lt;/strong>。&lt;/p>
&lt;p>例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Composable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fun&lt;/span> &lt;span class="nf">GameScreen&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">GameUiState&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 每次 state 变化，GameScreen 整体重组
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Header&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">userInfo&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Body&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">streamInfo&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>即使只有 &lt;code>streamInfo&lt;/code> 变了，&lt;code>Header&lt;/code> 也会因为父组件重组而被牵连（除非它加了 &lt;code>skips&lt;/code> 优化）。&lt;/p>
&lt;p>为了极致优化，我们可以引入 &lt;strong>Lambda Provider&lt;/strong> 模式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Composable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fun&lt;/span> &lt;span class="nf">GameScreen&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">stateProvider&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">GameUiState&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// GameScreen 只持有一个 lambda 引用，永远不会变 -&amp;gt; 跳过重组
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Header&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">stateProvider&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">userInfo&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Body&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">stateProvider&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">streamInfo&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>父组件只接收一个稳定的 Lambda 对象。&lt;/li>
&lt;li>取值操作被&lt;strong>推迟&lt;/strong>到了子组件内部真正需要数据的那一刻。&lt;/li>
&lt;li>结果：状态变化时，只有真正用到该状态的&lt;strong>最底层子组件&lt;/strong>才会重组，父组件稳如泰山。&lt;/li>
&lt;/ul>
&lt;h2 id="总结">总结
&lt;/h2>&lt;p>Compose 的性能优化往往隐藏在细节中：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>理解 Modifier 顺序&lt;/strong>：外层决定内层的命运。&lt;/li>
&lt;li>&lt;strong>避免无谓的 Layer&lt;/strong>：慎用 &lt;code>Modifier.alpha&lt;/code>，善用 &lt;code>Color.alpha&lt;/code>。&lt;/li>
&lt;li>&lt;strong>避重就轻&lt;/strong>：高频动画用 &lt;code>graphicsLayer&lt;/code> 跳过 Layout 阶段。&lt;/li>
&lt;li>&lt;strong>延迟读取&lt;/strong>：用 Lambda 推迟状态读取，通过「重组隔断」缩小刷新范围。&lt;/li>
&lt;/ol></description></item><item><title>Jetpack Compose 入门：声明式 UI、State 与重组</title><link>https://www.dust-zed.site/android/compose-basics/</link><pubDate>Sat, 25 Oct 2025 19:52:44 +0800</pubDate><guid>https://www.dust-zed.site/android/compose-basics/</guid><description>&lt;p>Compose 不是把 XML 换成 Kotlin，而是把 UI 从“命令式更新 View”换成“状态驱动界面”。理解 State 和重组之后，Compose 的很多 API 才会变得顺。&lt;/p>
&lt;h2 id="核心结论">核心结论
&lt;/h2>&lt;ol>
&lt;li>Composable 函数描述某个状态下 UI 应该长什么样。&lt;/li>
&lt;li>State 变化会触发读取它的 Composable 重组。&lt;/li>
&lt;li>&lt;code>remember&lt;/code> 负责在重组之间保存对象。&lt;/li>
&lt;li>状态应该尽量上提，让无状态 Composable 更容易复用和测试。&lt;/li>
&lt;li>重组不是重建整个屏幕，Compose 会尽量跳过输入未变化的部分。&lt;/li>
&lt;/ol>
&lt;h2 id="声明式-ui">声明式 UI
&lt;/h2>&lt;p>传统 View 更像这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">textView&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">text&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">button&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">isEnabled&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">isValid&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Compose 更像这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Composable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fun&lt;/span> &lt;span class="nf">UserCard&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">user&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">text&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Button&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">enabled&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">isValid&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">onClick&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">{})&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Submit&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你不再手动寻找某个 View 并修改它，而是声明：在当前状态下，UI 应该是什么样。&lt;/p>
&lt;h2 id="组合与重组">组合与重组
&lt;/h2>&lt;p>&lt;strong>组合&lt;/strong>是第一次执行 Composable，生成 UI 树。&lt;/p>
&lt;p>&lt;strong>重组&lt;/strong>是状态变化后，Compose 再次执行受影响的 Composable，让 UI 和新状态保持一致。&lt;/p>
&lt;p>关键点：&lt;/p>
&lt;ol>
&lt;li>重组可能频繁发生；&lt;/li>
&lt;li>Composable 应该保持轻量；&lt;/li>
&lt;li>不要在 Composable 函数体里直接执行不可控副作用；&lt;/li>
&lt;li>可以被重复调用的代码才适合放在 Composable 里。&lt;/li>
&lt;/ol>
&lt;h2 id="state-和-remember">State 和 remember
&lt;/h2>&lt;p>State 的作用是通知 Compose：值变了，需要更新 UI。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Composable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fun&lt;/span> &lt;span class="nf">Counter&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">var&lt;/span> &lt;span class="py">count&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">remember&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">mutableStateOf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Button&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">onClick&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">count&lt;/span>&lt;span class="o">++&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;count = &lt;/span>&lt;span class="si">$count&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里有两个角色：&lt;/p>
&lt;ol>
&lt;li>&lt;code>remember&lt;/code>：让 &lt;code>count&lt;/code> 在多次重组之间保留下来；&lt;/li>
&lt;li>&lt;code>mutableStateOf&lt;/code>：让 &lt;code>count&lt;/code> 变化时触发重组。&lt;/li>
&lt;/ol>
&lt;p>如果没有 &lt;code>remember&lt;/code>，每次重组都会重新创建状态。&lt;/p>
&lt;h2 id="状态提升">状态提升
&lt;/h2>&lt;p>当一个 Composable 同时负责保存状态和展示 UI 时，它会比较难复用。&lt;/p>
&lt;p>更推荐拆成：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Composable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fun&lt;/span> &lt;span class="nf">CounterScreen&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">var&lt;/span> &lt;span class="py">count&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">remember&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">mutableStateOf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CounterContent&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">count&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">count&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">onIncrease&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">count&lt;/span>&lt;span class="o">++&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Composable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fun&lt;/span> &lt;span class="nf">CounterContent&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">count&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Int&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">onIncrease&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">Unit&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Button&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">onClick&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">onIncrease&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;count = &lt;/span>&lt;span class="si">$count&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>CounterScreen&lt;/code> 管状态，&lt;code>CounterContent&lt;/code> 只负责展示和发事件。&lt;/p>
&lt;h2 id="重组触发">重组触发
&lt;/h2>&lt;p>重组常见来源：&lt;/p>
&lt;ol>
&lt;li>Composable 读取的 &lt;code>State&lt;/code> 发生变化；&lt;/li>
&lt;li>父 Composable 重组，并传入了新参数；&lt;/li>
&lt;li>&lt;code>Flow/LiveData&lt;/code> 被转换成 Compose State 后发出新值。&lt;/li>
&lt;/ol>
&lt;p>典型链路：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">ViewModel StateFlow 发出新值
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; collectAsStateWithLifecycle()
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; Compose State 更新
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; 读取该 State 的 Composable 重组
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; 子 Composable 根据参数变化决定是否重组
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="智能跳过">智能跳过
&lt;/h2>&lt;p>Compose 会尝试跳过输入没有变化的 Composable。&lt;/p>
&lt;p>更容易被跳过的参数：&lt;/p>
&lt;ol>
&lt;li>基本类型；&lt;/li>
&lt;li>&lt;code>String&lt;/code>；&lt;/li>
&lt;li>稳定的函数引用；&lt;/li>
&lt;li>&lt;code>@Stable&lt;/code> / &lt;code>@Immutable&lt;/code> 类型；&lt;/li>
&lt;li>Compose 内置稳定类型。&lt;/li>
&lt;/ol>
&lt;p>需要避免：&lt;/p>
&lt;ol>
&lt;li>每次重组都创建新的复杂对象；&lt;/li>
&lt;li>在参数里传不稳定集合；&lt;/li>
&lt;li>把过大的状态对象直接传到很深层级。&lt;/li>
&lt;/ol>
&lt;h2 id="回看清单">回看清单
&lt;/h2>&lt;ol>
&lt;li>Compose 用状态描述 UI，而不是手动更新 View。&lt;/li>
&lt;li>&lt;code>remember&lt;/code> 保存对象，&lt;code>State&lt;/code> 触发重组。&lt;/li>
&lt;li>状态尽量上提，UI 组件尽量无状态。&lt;/li>
&lt;li>副作用用 &lt;code>LaunchedEffect&lt;/code>、&lt;code>DisposableEffect&lt;/code> 等 API 管理。&lt;/li>
&lt;li>性能问题先判断是重组、布局还是绘制阶段。&lt;/li>
&lt;/ol></description></item><item><title>Compose Navigation 入门：路由、NavHost 与模块化导航</title><link>https://www.dust-zed.site/android/compose-navigation/</link><pubDate>Sat, 25 Oct 2025 16:01:56 +0800</pubDate><guid>https://www.dust-zed.site/android/compose-navigation/</guid><description>&lt;p>Compose Navigation 的重点不是“怎么跳页面”，而是如何把页面地址、返回栈、参数和模块边界组织清楚。页面少时可以直接写在一个 &lt;code>NavHost&lt;/code> 里，页面多时就需要模块化导航。&lt;/p>
&lt;h2 id="核心结论">核心结论
&lt;/h2>&lt;ol>
&lt;li>&lt;code>NavController&lt;/code> 负责执行导航和维护返回栈。&lt;/li>
&lt;li>&lt;code>NavHost&lt;/code> 承载导航图，把 route 映射到 Composable。&lt;/li>
&lt;li>route 是页面地址，建议统一封装，不要散落字符串。&lt;/li>
&lt;li>feature 模块可以通过扩展函数向主导航图注册自己的页面。&lt;/li>
&lt;li>复杂页面参数要谨慎传递，优先传 ID，再由目标页加载数据。&lt;/li>
&lt;/ol>
&lt;h2 id="核心组件">核心组件
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>组件&lt;/th>
&lt;th>职责&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>NavController&lt;/code>&lt;/td>
&lt;td>导航控制器，负责 &lt;code>navigate()&lt;/code>、&lt;code>popBackStack()&lt;/code> 和返回栈&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>NavHost&lt;/code>&lt;/td>
&lt;td>导航容器，根据当前 route 显示目标 Composable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>NavGraph&lt;/code>&lt;/td>
&lt;td>导航图，描述页面和页面之间的关系&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Route&lt;/code>&lt;/td>
&lt;td>目标页面地址&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>基础结构：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Composable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fun&lt;/span> &lt;span class="nf">AppNavHost&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">navController&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">NavHostController&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">rememberNavController&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">NavHost&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">navController&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">navController&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">startDestination&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;home&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">composable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;home&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">HomeScreen&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">onOpenDetail&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">id&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">navController&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">navigate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;detail/&lt;/span>&lt;span class="si">$id&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">composable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;detail/{id}&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">backStackEntry&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">id&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">backStackEntry&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">arguments&lt;/span>&lt;span class="o">?.&lt;/span>&lt;span class="n">getString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">DetailScreen&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="路由封装">路由封装
&lt;/h2>&lt;p>不要在项目里到处写字符串 route。更稳的方式是集中定义：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">object&lt;/span> &lt;span class="nc">HomeRoute&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">const&lt;/span> &lt;span class="k">val&lt;/span> &lt;span class="py">route&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;home&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">object&lt;/span> &lt;span class="nc">DetailRoute&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">const&lt;/span> &lt;span class="k">val&lt;/span> &lt;span class="py">route&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;detail/{id}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fun&lt;/span> &lt;span class="nf">create&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">String&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="n">String&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;detail/&lt;/span>&lt;span class="si">$id&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">navController&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">navigate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nc">DetailRoute&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">create&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果项目使用 Navigation 的类型安全路由能力，也可以用序列化对象表达 route：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Serializable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">data&lt;/span> &lt;span class="k">object&lt;/span> &lt;span class="nc">ForYouRoute&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="模块化导航">模块化导航
&lt;/h2>&lt;p>大型项目里，主导航图不应该知道每个 feature 内部的页面细节。可以让 feature 暴露一个注册函数：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fun&lt;/span> &lt;span class="nf">NavGraphBuilder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">forYouSection&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">onTopicClick&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">Unit&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">navigation&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">startDestination&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;for-you/feed&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">route&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;for-you&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">composable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;for-you/feed&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ForYouScreen&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">onTopicClick&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">onTopicClick&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>主工程只负责装配：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">NavHost&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">navController&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">navController&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">startDestination&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;for-you&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">forYouSection&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">onTopicClick&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">id&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">navController&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">navigate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;topic/&lt;/span>&lt;span class="si">$id&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样 feature 可以维护自己的子图，主导航只处理跨模块跳转。&lt;/p>
&lt;h2 id="参数传递原则">参数传递原则
&lt;/h2>&lt;p>推荐：&lt;/p>
&lt;ol>
&lt;li>route 里传轻量 ID；&lt;/li>
&lt;li>目标页面通过 ViewModel 根据 ID 加载数据；&lt;/li>
&lt;li>复杂对象放在共享数据层或持久化层；&lt;/li>
&lt;li>避免把大型 JSON 直接塞进 route。&lt;/li>
&lt;/ol>
&lt;p>原因：&lt;/p>
&lt;ol>
&lt;li>route 本质是地址，不适合承载大对象；&lt;/li>
&lt;li>进程重建后，参数越简单越容易恢复；&lt;/li>
&lt;li>页面之间传复杂对象会增加耦合。&lt;/li>
&lt;/ol>
&lt;h2 id="返回栈">返回栈
&lt;/h2>&lt;p>常用操作：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">navController&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">navigate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;detail/&lt;/span>&lt;span class="si">$id&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">navController&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">popBackStack&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>底部 Tab 场景通常需要：&lt;/p>
&lt;ol>
&lt;li>保存每个 Tab 的状态；&lt;/li>
&lt;li>避免重复创建同一个目的地；&lt;/li>
&lt;li>回到根目的地时恢复状态。&lt;/li>
&lt;/ol>
&lt;p>示例思路：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">navController&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">navigate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tab&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">route&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">popUpTo&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">navController&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">graph&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">startDestinationId&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">saveState&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">launchSingleTop&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">restoreState&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="回看清单">回看清单
&lt;/h2>&lt;ol>
&lt;li>&lt;code>NavController&lt;/code> 管行为，&lt;code>NavHost&lt;/code> 管展示，route 管地址。&lt;/li>
&lt;li>route 不要散落字符串，最好集中封装。&lt;/li>
&lt;li>feature 模块通过 &lt;code>NavGraphBuilder&lt;/code> 扩展函数注册自己的导航图。&lt;/li>
&lt;li>页面参数优先传 ID，不传复杂对象。&lt;/li>
&lt;li>底部 Tab 要关注返回栈复用和状态恢复。&lt;/li>
&lt;/ol></description></item></channel></rss>