往期回顾,专栏目录,更新动态,优惠政策,版权须知
温馨提示:如这是第一次接触《重学安卓》,可通过上述链接快速了解《重学安卓》专栏,获取 专栏目录、试读内容、更新动态 和 发展状况。
截至目前,专栏已对 体系化文章 实施 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 的关系。