96b200d595f390e7b2ce55f370e34608
重学安卓:就算不用 Jetpack Navigation,也请务必领略的声明式编程之美!

前言

很高兴见到你!

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

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

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

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

没想到,当天下午提的问题,当天晚上就收到了详尽的回复。(不要慌,关于此嘉宾以及解决方案,文末给出)😉

考虑到我在公开场合发文一贯秉持的原则是:为读者 首先明确状况、减少困扰、建立感性认识

因而,我绝不会一上来就解释针对 replace 的问题我们该怎么做,而是从 Navigation 的存在缘由、设计依据、职责边界 开始讲,这样才能帮助更多的人首先理解状况:

为什么要存在 Navigation?

Navigation 的存在是为了解决什么问题?

之前究竟是遇到了什么问题?

Navigation 引入后又发现了什么新问题?

……

文章目录一览

  • 前言
  • Navigation 的目标主要有 3 个
  • Navigation 问世前的混沌世界
  • Navigation 为什么能解决这三个问题?
  • 那 Navigation 具体是依赖什么机制运作的?
    • 1.定义声明式编程协议
    • 2.抽象和封装控制器代码
    • 3.对作用域的补充说明
  • 引入 Navigation 后的世界
  • 综上
  • 作为压轴分享的痛点解决方案
  • 作为额外附赠的使用说明

Navigation 的目标主要有 3 个

在《重学安卓》的前几期中,我们分别在

《重学安卓:你丢了 offer,只因拎不清 Activity 任务和返回栈》

《重学安卓:Intent 就是你的择偶标准啊!》

《重学安卓:我的碎片很听话,你的 Fragment 有自己的想法》

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

相信阅读完这几篇的朋友,对 Activity 和 Fragment 不同的 页面管理、路由跳转 的知识点印象 已经跑赢了 90% 的 Android 开发者。😉

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

它的目标主要有 3 个:

1.通过声明式编程,来确保 “应用内导航” 的一致性。

2.通过可视化编程,来直观地反映页面的路由关系。

3.通过抽象,来整合 Activity 和 Fragment 的路由跳转代码。

如果光是阅读了以上三点,你还是不太理解的话,那接下来我就分别介绍 99% 网文都不曾介绍的真实状况,来方便你迅速建立起感性的认识。😉

Navigation 问世前的混沌世界

在 Navigation 出现之前,想必大家用得最多的就是 YoKey 大神的 Fragmentation。

鉴于早期 Fragment 的坑防不胜防,而大家平时工作又没时间去确认状况,因而多是急忙地用上 Fragmentation “保平安”。

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

public void navigate(Fragment last, Fragment dest, 
                     Bundle bundle, int... customAnim) {
    dest.setArguments(bundle);
    FragmentTransaction t = mFragmentManager.beginTransaction();
    t.add(mFragmentContainerResId, dest, 
        dest.getClass().getSimpleName());
    t.hide(last);
    t.setCustomAnimations(customAnim[0], customAnim[1], 
        customAnim[2], customAnim[3]);
    t.addToBackStack(null);
    t.commit();
}

那这样造成什么问题呢?

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

FragmentNavigator.getInstance().navigate(
     AlbumFragment.this, new PlayFragment(), bundle,
     R.anim.slide_in_right,
     R.anim.slide_out_left,
     R.anim.slide_in_left,
     R.anim.slide_out_right
);

如果你对 “一致性” 的概念不太理解的话,那么请回顾一下大学时期 数据库课程中提到的 “存储过程”,它就是 典型的 “一致性” 封装:将一套连贯的 SQL 序列封装在一个 “API” 中,外部人员只需调用这个 “API”,而无需知道内部具体发生了什么。

这避免了程序员 A 修改了某 SQL 语句,而程序员 B 没跟上。并且,“API” 的管理人只需一处修改,就能做到处处生效。

此外,一个项目的 Fragment 可能会很多(例如在上周的新项目中,我新建了多达 25 个页面),日后接手项目的同事,在缺乏文档的情况下,很难第一时间掌握项目状况、并定位到问题所在页面:

顺藤摸瓜、通过 menifest 和 布局文件找到程序入口和对应的 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 属性来描述默认值,当没有传参的时候。

```xml

<fragment
    android:id="@+id/mainFragment"
    android:name="com.kunminx.player.page.MainFragment"
    android:label="fragment_main"
    tools:layout="@layout/fragment_main">

    <action
        android:id="@+id/action_mainFragment_to_albumFragment"
        app:destination="@id/albumFragment"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right" />

    <argument
        android:defaultValue="true"
        app:argType="boolean"
        android:name="isAuto" />

</fragment>

<fragment
    android:id="@+id/albumFragment"
    android:name="com.kunminx.player.page.AlbumFragment"
top Created with Sketch.