Jetpack Navigation3 类型安全深链实战

Nav3 深链 API 用类型安全 matcher 将 URI 解析为 NavKey,并懒构造合成返回栈。
Jetpack Navigation3 把 backstack 控制权交给开发者,深链处理的责任也随之转移。Google 团队分享了 Nav3 深链 API 的设计:从 recipe 到正式 API 的 graduation 流程;用 DeepLinkRequest、DeepLinkMatcher 和 URIDeepLinkMatcher 借助 Kotlin Serialization 把 URI 占位符解析为类型安全的 NavKey;多个深链对应多个 matcher,匹配结果可比较并用 maxOrNull 选出最佳匹配;面对难缠 URL 可自定义 DeepLinkSerializer 或重写 matchArguments 预处理;合成返回栈通过 withBackStack 懒构造,兼顾每深链自定义逻辑与启动性能;核心逻辑位于公共 KMP 代码中,可跨平台共享。
Jetpack Navigation3 在架构上把 backstack 的控制权交给开发者,这种 compose-first 的设计让导航逻辑更灵活,但也把深链(Deep Link)处理的责任一并转移了过来。Google Jetpack 架构团队工程经理 Ian Lake 与 Nav3 技术负责人 Clara Fogh 在一场技术分享中,详细拆解了 Nav3 深链 API 的设计思路:如何从 URI 中解析出类型安全的 NavKey,以及如何为深链构建合理的合成返回栈。
深链 API 的设计哲学:从 Recipe 到正式 API
Nav3 团队的一个核心目标是「不挡路」。当开发者提出新需求时,团队倾向于先写 recipe——一份说明如何在不改动库本身的情况下实现该功能的指南,而不是立刻新增 API。Nav3 recipes 仓库在 GitHub 上已经积累了超过 35 个常见用例,涵盖多返回栈、底部弹窗等场景。
早在 Nav3 1.0 发布时,深链 recipe 就已经存在,因为团队清楚这对很多开发者很重要。但 recipe 意味着需要把一堆辅助方法复制粘贴进项目,于是团队启动了一个称为「graduation」的流程:评估这些辅助方法如果成为 Nav3 的一部分,API 应该长什么样。Ian Lake 提到,他自己为 Nav2 和 Nav3 都写过深链实现,加上 recipes 里的版本,前后迭代了三次,目的就是让 API 与 Nav3 的 compose-first 风格自然契合。
graduation 的好处在于没有强制迁移:已经复制过 recipe 代码的开发者可以继续用,而新项目则能直接使用经过打磨的官方 API。

Recipe(配方) 是 Jetpack 库团队常用的一种轻量知识产出形式:它是一份独立的指南或示例代码,展示如何仅使用库的公开 API 来实现某个常见需求,而不必等待库本身新增功能。Recipe 通常托管在 GitHub 仓库中,以文档加可运行代码的形式呈现,开发者可以直接复制其中的辅助方法到自己的项目。这种做法的价值在于快速响应社区反馈、验证需求是否普遍,同时避免过早把实验性 API 固化进库的稳定接口。当某个 recipe 被反复使用、需求足够明确时,团队才会启动 graduation(毕业) 流程,把它升级为正式 API——这意味着重新设计接口形态、考虑向后兼容与 KMP 支持,并经过多轮内部评审。因此 recipe 与正式 API 之间不是替代关系,而是并存:旧代码继续可用,新项目获得更完善的官方支持。
从 Intent 到 NavKey:类型安全匹配的核心链路
用户点击一个 Web URI 后,Android 框架要求应用为每个支持的深链声明 intent filter。真正有趣的部分发生在深链触发 Activity 启动之后。
Nav3 把深链输入封装成一个 KMP 兼容的 DeepLinkRequest 类,在 Android 上可以用 Intent 构造。接下来的问题是:给定一个请求,如何知道该把哪个 NavKey 放进返回栈?DeepLinkMatcher 承担了这个职责,它接收请求并输出匹配结果,其中包含匹配到的 NavKey。
针对 Web URI,URIDeepLinkMatcher 封装了全部转换逻辑。它的第一个参数是 URI 模式。以 ADS 应用为例,指向首页的深链可以直接用精确 URI 匹配,省略 http scheme 即可同时匹配 http 和 https。若要深链到某个 ADS 会话,URI 中含有需要提取的 session ID,此时模式用花括号作为占位符,且占位符名称必须与 NavKey 属性的名称一致,解码器才能正确对应。
URIDeepLinkMatcher 借助 Kotlin Serialization,用 Kotlin decoder 把输入 URI 反序列化成 NavKey 实例,因此还需要向 matcher 传入该 NavKey 的 serializer。
Kotlin Serialization 是 Kotlin 官方的序列化框架,通过编译器插件为标注了 @Serializable 的类生成序列化器(serializer),支持 JSON、Protobuf 等多种格式,并且是 Kotlin Multiplatform 的一等公民。在 Nav3 深链场景中,它被用作 URI 字符串与 NavKey 数据类之间的桥梁:URIDeepLinkMatcher 把 URI 中的占位符值当作字段输入,交给 Kotlin Serialization 的解码器(decoder)反序列化成 NavKey 实例。之所以要求占位符名称与属性名一致,是因为默认解码器按字段名匹配;如果名称不一致,就需要自定义 DeepLinkSerializer 来显式指定 serial name。Kotlin Serialization 对基础类型(布尔、整数、浮点、枚举)提供开箱即用的支持,解析失败时会抛出异常,matcher 捕获后返回 null,从而实现「无法解析即不匹配」的自动失败语义。可选参数通过属性默认值表达,解码器在字段缺失时会回退到默认值。
多深链并存与最佳匹配选择
多个深链应写成多个 matcher,每个深链一个。例如会话 key 可能有两个 URI 模式——一个旧的 newsletter 模式和一个常规 Web URI,再加上首页深链,三个 matcher 放进同一个列表,匹配时遍历列表逐一调用 match,用 mapNotNull 过滤掉空结果。
多数情况下列表中只有一个非空匹配,但也存在多个有效匹配的场景:比如新增一个 keynote 页面,复用会话 URI 模式但把 ID 占位符换成静态的 keynote 路径,同一个请求就会同时匹配会话 key(ID 等于 keynote)和 keynote key。由于匹配结果实现了 comparable,可以用 maxOrNull 在所有有效匹配中选出最佳匹配,同时处理没有任何匹配的情况。
静态的 matcher 列表只适合闪电演讲式的演示。实际项目中,屏幕可能分散在多个模块,团队期望这些模块通过 DI 系统向全局 matcher 列表贡献自己的条目。据 Ian Lake 介绍,recipes 仓库中的 Hilt、Metro 和 Koin 示例都已更新,加入了深链支持。
处理「糟糕」深链:自定义序列化与预处理
Kotlin Serialization 提供了扎实的基础:布尔、整数、浮点乃至枚举等基础类型开箱即用,若深链值无法解析为目标类型,匹配会自动失败并返回 null。可选参数则可以让属性带默认值。
团队刻意把 Web URL 的结构与代表匹配结果类型安全类解耦,因为 URL 可能由其他团队维护。现实中存在不少难缠的深链——比如附带 base64 编码图片,或塞满 JSON blob。面对无法修改的 URL 又要提取信息,Nav3 提供了两条路径。
其一是扩展 DeepLinkSerializer 类,精确控制某个参数如何从字符串反序列化为类型安全属性。需要实现三个方法:选择唯一的 serial name、实现 deserialize(接收来自 Web URL 的字符串并构造对象)、实现 serialize(反向操作,用于同一 key 出现在 rememberNavBackStack 等场景时)。[截图位于此处] 之后把自定义 serializer 应用到特殊类型的属性上,WebURIDeepLinkMatcher 会处理其余部分。

其二是在数据进入 Kotlin Serialization 之前就做预处理——继承 URIDeepLinkMatcher 并重写 matchArguments 方法。输入是 URI 路径中所有占位符对应的原始字符串,可以在调用 super.matchArguments 之前编辑这些值。比如面对一个 JSON blob,实际只需要其中的 session ID,这样就能绕开复杂序列化逻辑。
合成返回栈:兼顾体验与性能的懒构造
仅有一个导航 key 的返回栈未必带来好的用户体验。要保证正确的返回行为,需要合成返回栈(synthetic backstack)——其中的 key 用户从未手动导航过去,但确实是他们手动导航时会经过的路径。例如点击一个即将开始会话的通知,用户被直接带到该会话,而触发系统返回时,大概希望先回到首页再退出到桌面,这个首页就成为合成返回栈的一部分。
这里有两个相互竞争的需求:不同深链需要不同的合成返回栈逻辑(给每个深链都加首页,在指向设置子页面的深链上就不够用);而每一毫秒的延迟都意味着真金白银的损失,不能为每个匹配结果都急切地计算返回栈。

经过几次试错,团队找到了平衡点:无论哪种 matcher,都可以调用 withBackStack,以懒加载方式基于给定匹配结果构造返回栈。每个 matcher 各自拥有自己的 withBackStack 调用。若所有 matcher 都这样处理,会得到一个 back stack matcher 列表,最终匹配结果是 BackStackMatchResult——它是匹配结果的子类,新增 backStack 属性,即 withBackStack lambda 生成的完整返回栈。这种分层让 matcher 内部逻辑与自定义返回栈逻辑彻底解耦。

过滤器、静态匹配与 KMP 共享
Nav3 还提供了一些实用能力。可以为任意 matcher 添加过滤器,基于 action、MIME 类型或自定义条件快速短路匹配逻辑——这发生在任何 URI 解析或 Kotlin Serialization 之前,能有效节省开销。
对于不需要解析就能得到 key、但仍想应用过滤器的场景,有 StaticKeyDeepLinkMatcher,适合只处理 action 或 MIME 类型的深链,比如一个能打开 JPEG 图片的照片编辑应用。深链请求还支持附加自定义 extras,自定义过滤器和 DeepLinkRequest 子类都可以在逻辑中使用,比如混入 A/B 测试框架或外部信号,且 extras 拥有完整的类型安全 API。
值得一提的是,上述几乎所有逻辑都位于 Nav3 的公共 KMP 代码中,matcher 与返回栈逻辑可以跨平台共享。团队也将深链 API 集成进了 DI recipes 以及 nowinandroid 示例应用,感兴趣的开发者可以在 Nav3 recipes 仓库中进一步探索。
KMP(Kotlin Multiplatform) 允许同一份 Kotlin 代码编译到 Android、iOS、JVM、JS 等多个目标平台,公共代码放在 commonMain 源集中,平台相关实现通过 expect/actual 机制提供。Nav3 把深链匹配、返回栈构造等核心逻辑放在公共 KMP 代码中,意味着这些逻辑不依赖 Android 的 Intent 或 URI 类,而是使用 KMP 兼容的抽象(如 DeepLinkRequest)。在 Android 上,DeepLinkRequest 可以由 Intent 构造;在其他平台上,则可由各自平台的深链表示构造。这样 matcher 列表、withBackStack 懒构造、过滤器短路等逻辑可以跨平台复用,只有最外层的平台入口(如 Android 的 intent filter 声明与 Activity 启动)需要平台特定代码。对于希望把导航逻辑下沉到共享层的多平台项目,这一点尤为关键。
相关推荐

零基础用WorkBuddy做AI漫剧:技能包保姆级安装教程
零基础如何用WorkBuddy制作AI漫剧?本文详解WorkBuddy安装、Skill技能包导入与调用全流程,附剧本生成实操演示与人工修改技巧,帮你跑通AI漫剧从剧本到分镜的第一步。

WorkBuddy智能体一句话跑通GEO数据库挖掘全流程
WorkBuddy智能体实现GEO数据库挖掘全自动化:从数据搜索、下载整理到批次校正、差异分析、共表达网络、特征基因筛选及免疫浸润分析,一句话提示词即可生成完整生信分析报告,大幅降低生信分析门槛。

PC内存被浪费?Hacker News热议集中式数据中心之争
Hacker News用户提出个人电脑内存被浪费、应集中到数据中心的观点,引发关于集中式与分布式计算资源分配的讨论。本文分析该观点背后的逻辑、集中化的代价,以及AI时代内存资源分配的新张力。