重学安卓2-5:柳暗花明 Jetpack Navigation 打开方式与缺陷分析

作品简介
往期回顾专栏目录更新动态优惠政策版权须知
温馨提示:如这是第一次接触《重学安卓》,可通过上述链接快速了解《重学安卓》专栏,获取 专栏目录、试读内容、更新动态 和 发展状况。
截至目前,专栏已对 体系化文章 实施 3310 余次修订,数十位群友告诉我,受专栏启发 亦开启了写作之路。群里不定期会有小伙伴讨论适配问题、分享原创开源库 和 提供内推机会,订阅后可随时进群交流。

·

重要提示
阅读本文最佳时机是,您已吃过 阅读 “源码” 或 “源码分析文” 时 找不到头绪的苦
您还没吃过苦,那您先不要着急阅读本文。您得吃过苦,才会有体会。
在您吃够这方面苦后,您才有机会发现,本文正是专用于解决 “如何找到正确打开方式” 的困扰。
我们绝不通篇贴源码,而是基于广泛实践和反思,在累积过大量样本 乃至足以排除掉所有干扰信息后,点到为止揭露 Navigation 框架最核心本质,方便您理解其真实的存在意义,乃至笃信使用项目中。
关于 Navigation 在项目中实践,请另行查阅《GitHub : Jetpack-MVVM-Best-Practice》源码以及《架构模式实践》篇 完整解析。

前言


上周我在新项目中用上 Jetpack Navigation,对它的 声明式设计 是十分喜欢,

然而没想到,就在用得正爽时,发现它居然是通过 replace 来完成页面跳转,除此之外,就没有 add hide 的方式可选。这导致了 页面返回时,每次都要重绘,大概率造成转场卡顿

在 google sample 的 issue 中发现一年前就有人提过这个问题,但官方并没有解释这么设计的缘由,

于是我转而去咨询今天要介绍的一位嘉宾,看看他是如何看待这个设计,以及如果想通过 Navigation 来 add hide,除了重写代码,还有没有别的招,

没想到,当天下午提的问题,当天晚上就收到了详尽的回复

关于此嘉宾及解决方案,文末给出。

考虑到本专栏文章的一贯原则是:为读者 首先拨开迷雾、构建 “来龙去脉” 画面感、乃至理解本质

因此在具体探索 replace 的解决方案前,我们先就 Navigation 的 存在缘由、设计依据、职责边界 做个完整铺垫,以便理解:

为什么要存在 Navigation?
Navigation 的存在是为了解决什么问题?
之前究竟是遇到了什么问题?
Navigation 引入后又发现了什么新问题?
……


文章目录一览

  • 前言
  • Navigation 的目标主要有 3 个
  • Navigation 问世前的混沌世界
  • Navigation 为什么能解决这三个问题?
  • Navigation 具体依赖的机制?
  • 1.定义声明式编程协议
  • 2.抽象和封装控制器代码
  • 3.对作用域的补充说明
  • 引入 Navigation 后的世界
  • 综上
  • 作为压轴分享的痛点解决方案
  • Note 2019.8.7 加餐:
  • 作为额外附赠的使用说明
  • Note 2019.11.4 加餐:
  • 对官方使用 replace 的缘由的理解 和 专属解决方案
  • Note 2020.3.31 加餐:
  • 不建议通过复用 View 的方式优化 onCreateView 的缘由
  • Note 2020.07.16 加餐:
  • GitHub 上的 Navigation Add Hide 修改版的致命通病 和 独家解决方案
  • Note 2021.07.10 加餐:
  • Navigation 有什么难以替代的好处?

Navigation 的目标主要有 3 个


前几期,我们分别在

《Activity 任务和返回栈》篇《Intent》篇《Fragment 本质》篇

中深入介绍了 Activity 和 Fragment 由于生存环境、沟通目标的差异,而在 “页面管理” 和 “路由跳转” 的 “设计依据、职责边界、乃至相互间关系” 上 存在的区别

而今天我们要介绍的 Navigation,正是基于上述分裂的环境,为解决 “应用内导航” 的问题而存在。

它的目标主要有 3 个:

1.通过声明式编程,来确保 “应用内导航” 的一致性。
2.通过可视化编程,来直观反映页面的路由关系。
3.通过抽象,来整合 Activity 和 Fragment 的路由跳转代码。

上述三点,如暂无体会,那接下来,请跟随我脚步一起来铺垫 Navigation 来龙去脉。

Navigation 问世前的混沌世界


Navigation 问世前,想必大家用得最多的就是 YoKey 大神的 Fragmentation。

鉴于早期 Fragment 的坑防不胜防,而大家平时忙于业务、无暇确认状况,乃至多是闭着眼用上 Fragmentation “保平安”。

考虑到现如今 Fragment 的 bug 早已被修复,因而从适配 AndroidX 开始,我便转而采用 “原生方式” 实现路由管理。例如:


那这会造成什么问题?

例如开发一款音乐播放器,在 “首页” 和 “专辑列表页” 等多个源 Fragment 中 需要跳转到该页面,那么在每个源页面中 我都需要动态注入同样的 “转场动画” 等样板代码,那么日后随着源页面的激增,当源页面 A 中改了转场动画,源页面 B 中忘了改转场动画,就会造成不一致的问题


此外,一个项目的 Fragment 可能会多达 30、40 个甚至更多,日后接手项目的同事,在缺乏文档的情况下,很难第一时间掌握项目状况、并定位到问题所在页面:

顺藤摸瓜、通过 “manifest 和 布局文件” 找到程序入口和对应的 Fragment,并不总是最优解。

再者,如上一节提到的,Activity 和 Fragment 的生存环境不同,Activity 因为是组件,可被允许和其他 App 的组件通信,而在设计之初就考虑到 是面向跨进程通信的组件化管理,那么无论是任务栈、返回栈管理,还是路由跳转的设计,都和 Fragment 有着天壤之别,

因而,就应用内的导航来说,如果你要允许 Activity 也来专职视图控制器,那么它同样也会遇到上述 “样板代码” 的问题。并且,将 “应用内导航” 的工作托管给两套 API 显然不方便。

Navigation 正是为解决上述三个问题而存在。

Navigation 为什么能解决这三个问题?


《你用不惯 RxJava,只因缺了这把钥匙》 中我们介绍到,RxJava 操作符同 SQL 一样,本质上是声明式编程,也即你 只需告诉后台要做什么,而无需告诉后台怎么做

“怎么做” 的逻辑已在后台统一封装好,你只需遵从 “协议” 来定义你的声明、并在恰当的地方调用即可,后台会自动根据声明来执行相应的代码

注:编程语言的本质就是一种协议,操作符是一种协议,SQL 是一种协议,Navigation Graph 也是种协议。

正因为是声明式编程,所以你可以用统一的 XML 声明,去匹配不同的 Java 实现。所以这让 Activity 和 Fragment 的路由管理的抽象成为了可能。

并且,正因为是声明式编程,所以更方便要求你填写特定的属性,以便让后台支持如 “可视化编程” 这样功能的展现。

Navigation 具体依赖的机制


首先,既然要声明式编程,那么第一步要做的当然是

1.定义声明式编程协议


我们不妨站在源码设计师的角度来想一想,路由跳转从抽象意义上讲,都包含哪些必要的元素?

—— 源地址,目标地址,携带参数,转场动画,启动模式

主要是上述这 5 个部分。因而我们先定义出

fragment、action、argument 这三种元素。

其中,fragment 元素对应着一个源 Fragment,

需要属性 name 来指明源地址;

需要属性 id 来帮助其他 Fragment 的 action 元素链接到自己、从而帮助后台找到自己。

同时,每个 fragment 需要包含 action 元素 和 argument 元素,来分别描述 fragment 可以执行跳转的目标,以及携带的参数。

在 action 元素中,我们

需要属性 destination 来指明前往的目标 id;

需要 popUpTo 来指明需要跨级返回的源 id;

需要 诸如 launchSingleTop 的属性来表明页面的启动模式;

需要 anim 属性来声明转场动画;

需要属性 id 来帮助后台发现和执行这个 action。

在 argument 元素中,我们

需要 name 属性来描述参数名;

需要 argType 属性来描述参数类型;

需要 defaultValue 属性来描述默认值,当没有传参的时候。


没错,梳理一遍就会发现,graph XML 的配置其实简单。
同时,不要忘了,为了支持 “可视化编程”,我们还需在 fragment 元素中定义 tools:layout 属性,这样我们就可以在 Android Studio 中预览这些 fragment 的关系。
创作时间: