<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Navigation on zed的博客</title><link>https://www.dust-zed.site/tags/navigation/</link><description>Recent content in Navigation on zed的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sat, 25 Oct 2025 16:01:56 +0800</lastBuildDate><atom:link href="https://www.dust-zed.site/tags/navigation/index.xml" rel="self" type="application/rss+xml"/><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>