​一. 背景

从项目发展来看,随着业务迭代,我们 Fragment 之间的跳转越来越多:

Android 客户端组件化之后,由于团队基础的架构单个的业务组件规约是一个Activity 当中包含着多个Fragment。因此随着业务的迭代Fragment会越来越多,Fragment 之间的跳转,按照现有的方法,是采用封装的ReplaceFragment 方法,会有大量的跳转逻辑埋藏在代码里面令人头疼,也严重不符合单一职责。需要一种新的方案来进行Fragment 之间的管理。

从 Android 发展来看,简单的 Intent 无法满足界面跳转需求:

从一个界面跳转到另一界面,这是安卓开发的基础部分。过去,你可以使用 Intent 交互来完成此操作,在简单的情况下,例如单击按钮,这很容易。但如果你想做一些稍微复杂的事情呢?例如,像底部导航这样常见模式。你需要确保不仅你的底部导航视图可以真的导航,而且还要突出显示正确的按钮。而且它以统一的方式处理后台堆叠,这样用户就不会失去方向或迷茫。像这样的案例是新导航组件闪耀的地方。

Navigation 的出现:

该导航组件是一个可简化安卓导航的插件和工具。除了使底部导航等常见模式的设置更容易之外,该组件还处理后台堆栈,fragment 切换,参数传递,基于导航的动画和深层链接。重要的是,它会收集所有这些导航信息,并将其放在你应用程序的一个可视化的导航图。并支持 Deeplink 。

二.Navigation 的配置

1.在project的build.gradle 添加如下:

f37ec2db7baee585a1fd81d03fb30ca7.png

2.在app的build.gradle 添加如下:

8754f2390b5752df0d3dac2a269d85f0.png

三.Navigation 的简单跳转

1.添加导航图(类似iOS开发中的StoryBoard):

  • 击res目录,选择New > Android resource file
  • New Resource对话中输入文件名nav_graph_main.xml,选择Resource type为Navigation

点击OK后IDE会在navigation目录下生成nav_graph_main.xml文件

70a8f55672db989d81f14b602d20a62c.png

2.在Activity布局中指定Navigation的宿主(Host):

4d7ae8f2d26513c3ece35cb56c6155a9.png

其中,fragment的name一定要是androidx.navigation.fragment.NavHostFragment,app:navGraph输入刚刚生成的导航图位置。

覆写onSupportNavigateUp()方法,如果app:defaultNavHost="true" 表示使用默认的导航host,自动覆盖Activity的back按钮,不用再覆写[AppCompatActivity.onSupportNavigateUp()]

1539dcbfa3bf928610136642d60f9aeb.png

3.自己创建多个fragmet.xml 如下

763aad5b10921266b2b643d8f20f3155.png

内容类似如下

e7c2c78e4b525b349bc3e900ede4adf8.png

4.Fragment.class 代码实现如下

73939618a6d4beea45c4ffe95133fbd0.png

5.Fragment 当中的跳转传参

传参通过Buddle,详见如下:

e8c15a5ba3ad2e98110ab6544b81f5b0.png

接收参数如下:

323989499696b24e951cb1600131c65d.png

6.返回上前一个Fragment 方法

9fe68541a63338ab7c40881b757d9f0b.png

四、默认的参数跳转设置:

描述:可以用来保证参数不为空

在project的build.gradle当中添加

d7bf4509f49c25aa670556d883e6434b.png

在app的build.gradle 当中添加

7db0af8b9ec2f226be8f9acacf666cdc.png

在nav_graph_main.xml 当中添加

31a572524cb34cff77ab1c20e5b9091b.png

argument 有三个属性 name、defaultValue 和 type,

  • name 就是名字到时会生成这个名字的 set 和 get 方法,
  • defaultValue 是默认值,
  • type 就是数据类型,有以下几种可以使用

五、设置跳转的动画

在nav_graph_main.xm当中配置

610f5e475edb9939e07c14f40a23ae02.png

六、导航文件 nav_graph_main.xm 说明

id: 就像写布局的 id 那样需要给个 id 才能找到它

name: 指定哪个 Fragment 类名

tools:layout: fragment的layout

id :就是这个 action 的 id。

destination:是目的地,要跳转到哪里的。

还可以设置动画

点击下面的Design查看下:

58d8d324eb4355f262ccf46fa0197af1.png
a9dd34994b7181753d59193bcfd20e30.png

七、Deeplink 的支持

在AndroidMainifest.xml 里面设置:

dd6c7e7f13242dcfbe3dceeabdce72be.png

在 Navigation 里面设置:

f36dcd11d70e8ef0a4b3a96a7d7e618c.png

八、类似 ViewPage 的支持

635cbe0b385783bb9eb5656fb29e5ac4.png

九、源码分析

我花了一些时间绘制了 Navigation的UML类图,我坚信,这种方式能帮助你我 更深刻的理解 Navigation的整体架构:

13c13dba903218d7946366ba5206baa4.png

设计 NavHostFragment

NavHostFragment 应当有两个作用:

  • 作为Activity导航界面的载体
  • 管理并控制导航的行为

前者的作用我们已经说过了,我们通过在NavHostFragment的创建时,为它创建一个对应的FrameLayout作为 导航界面的载体

Group container, @Nullable Bundle savedInstanceState) {FrameLayout frameLayout = new FrameLayout(inflater.getContext());  frameLayout.setId(getId());  return frameLayout;   }

public View onCreateView(@NonNull LayoutInflater inflater, @Nullable View

我们都知道代码设计应该遵循 单一职责原则,因此,我们应该将 管理并控制导航的行为 交给另外一个类,这个类的作用应该仅是 控制导航行为,因此我们命名为 NavController

Fragment理应持有这个NavController的实例,并将导航行为 委托 给它,这里我们将 NavController 的持有者抽象为一个 接口,以便于以后的拓展。

于是我们创造了 NavHost 接口,并让NavHostFragment实现了这个接口:

public interface NavHost {    NavController getNavController();}

为了保证导航的 安全,NavHostFragment 在其 作用域 内,理应 有且仅有一个NavController 的实例

这里我们驻足一下,请注意API的设计,似乎 Navigation.findNavController(View),参数中传递任意一个 view的引用似乎都可以获取 NavController——如何保证 NavController 的局部单例呢?

事实上,findNavController(View)内部实现是通过 遍历 View树,直到找到最底部 NavHostFragment 中的NavController对象,并将其返回的:

private static NavController findViewNavController(@NonNull View view) {        while (view != null) {           NavController controller = getViewNavController(view);            if (controller != null) {                return controller;            }            ViewParent parent = view.getParent();            view = parent instanceof View ? (View) parent : null;         }        return null;  }    

设计 NavController

站在 设计者 的角度,NavController 的职责是:

  • 1.对navigation资源文件夹下nav_graph.xml的 解析
  • 2.通过解析xml,获取所有 Destination(目标点)的 引用 或者 Class的引用
  • 3.记录当前栈中 Fragment的顺序
  • 3.管理控制 导航行为

NavController 持有了一个 NavInflater ,并通过 NavInflater 解析xml文件。

这之后,获取了所有 Destination(在本文中即Page1Fragment , Page2Fragment , Page3Fragment ) 的 Class对象,并通过反射的方式,实例化对应的 Destination,通过一个队列保存:

private NavInflater mInflater;  //NavInflaterprivate NavGraph mGraph;        //解析xml,得到NavGraphprivate int mGraphId;           //xml对应的id,比如 nav_graph_main//所有Destination的队列,用来处理回退栈 private final Deque mBackStack = new ArrayDeque<>();

这看起来没有任何问题,但是站在 设计者 的角度上,还略有不足,那就是,Navigation并非只为Fragment服务

先不去吐槽Google工程师的野心,因为现在我们就是他,从拓展性的角度考虑,Navigation是一个导航框架,今后可能 并非只为Fragment导航

我们应该为要将导航的 Destination 抽象出来,这个类叫做 NavDestination ——无论 Fragment 也好,Activity 也罢,只要实现了这个接口,对于NavController 来讲,他们都是 Destination(目标点)而已。

对于不同的 NavDestination 来讲,它们之间的导航方式是不同的,这完全有可能(比如Activity 和 Fragment),如何根据不同的 NavDestination 进行不同的 导航处理 呢?

NavDestination 和 Navigator

有同学说,我可以这样设计,通过 instanceof 关键字,对 NavDestination 的类型进行判断,并分别做出处理,比如这样:

if (destination instanceof Fragment) {  //对应Fragment的导航} else if (destination instanceof Activity) {  //对应Activity的导航}

这是OK的,但是不够优雅,Google的方式是通过抽象出一个类,这个类叫做 Navigator

public abstract class Navigator {   //省略很多代码,包括部分抽象方法,这里仅阐述设计的思路!    //导航    public abstract void navigate(@NonNull D destination, @Nullable Bundle args,                                     @Nullable NavOptions navOptions);   //实例化NavDestination(就是Fragment)   public abstract D createDestination();    //后退导航    public abstract boolean popBackStack();}

Navigator(导航者) 的职责很单纯:

  • 1.能够实例化对应的 NavDestination
  • 2.能够指定导航
  • 3.能够后退导航

你看,我的 NavController 获取了所有 NavDestination 的Class对象,但是我不负责它 如何实例化 ,也不负责 如何导航 ,也不负责

如何后退 ——我仅仅持有向上的引用,然后调用它的接口方法,它的实现我不关心。

FragmentNavigator为例,我们来看看它是如何执行的职责:

public class FragmentNavigator extends Navigator {     //省略大量非关键代码,请以实际代码为主!   @Override   public boolean popBackStack() {        return mFragmentManager.popBackStackImmediate();    }​  @NonNull   @Override  public Destination createDestination() {    // 实际执行了好几层,但核心代码如下,通过反射实例化Fragment       Class extends Fragment> clazz = getFragmentClass();      return  clazz.newInstance();  }​  @Override  public void navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions)       // 实际上还是通过FragmentTransaction进行的跳转处理      final Fragment frag = destination.createFragment(args);      final FragmentTransaction ft = mFragmentManager.beginTransaction();      ft.replace(mContainerId, frag);      ft.commit();      mFragmentManager.executePendingTransactions();   }}

不同的 Navigator 对应不同的 NavDestinationFragmentNavigator 对应的是 FragmentNavigator.Destination,你可以把他理解为案例中的 Fragment ,有兴趣的朋友可以自己研究一下。

总结:

优点:减少Fragment 当中跳转相关的代码 ,更符合单一职责设计原理。缺点:多了一个xml。

Demo 地址 :

Navigation 跳转Demo:

https://github.com/YuriyPiKachu/NavigationDemo.git

Navigation 底部导航栏切换Demo :

https://github.com/YuriyPiKachu/NavigationAdvancedSample.git

Navigation 官网:

https://developer.android.google.cn/guide/navigation/

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐