您现在的位置是:首页 >技术教程 >【见微知著】Android Jetpack - Navigation的架构设计网站首页技术教程

【见微知著】Android Jetpack - Navigation的架构设计

苏火火丶 2024-09-17 12:01:06
简介【见微知著】Android Jetpack - Navigation的架构设计

在这里插入图片描述

前言:人总是理所当然的忘记,是谁风里雨里,一直默默的守护在原地。

前言

Navigation 作为 Android Jetpack 组件库中的一员,是一个通用的页面导航框架。为单 Activity 架构而生的端内路由导航,用来管理 Fragment 的切换,并且可以通过可视化的方式,看见 App 的交互流程。今天主要来分析 Navigation 的简单用法和内部原理。

Navigation 是 Jetpack 组件库众多优秀组件之一,它的定位是页面路由导航,有以下几点优势:

  • 支持 Activity,Fragmegnt,Dialog 跳转;
  • 支持跳转时数据的安全性,safeArgs 安全数据传递;
  • 自定义拓展 Navigation
  • 支持深度链接 Deeplink,Deeplink 提供了页面直达的能力;
  • 支持可视化编辑,与 Android studio 绑定,提供了可视化编辑界面;
  • 回退堆栈管理,支持逐个出栈,也支持回到某个页面。

一、基本使用

  1. build.gradle 文件中添加依赖,目前版本是 2.5.3
implementation 'androidx.navigation:navigation-fragment:$version'
implementation 'androidx.navigation:navigation-ui:$version'
  1. 在 res 文件夹下新建一个 navigaton 文件夹,创建导航图 mobile_navigation.xml:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/mobile_navigation"
        app:startDestination="@+id/nav_main">

    <fragment
            android:id="@+id/nav_main"
            android:name="com.sum.navigation.MainFragment"
            android:label="HomeNavFragment"
            tools:layout="@layout/fragment_home_nav" />
    <activity
            android:id="@+id/nav_activity"
            android:name="com.sum.navigation.NavActivity"
            android:label="NavActivity"
            tools:layout="@layout/activity_nav" />
    <fragment
            android:id="@+id/nav_fragment"
            android:name="com.sum.navigation.NavFragment"
            android:label="FindNavFragment"
            tools:layout="@layout/fragment_home_nav" />
    <dialog
            android:id="@+id/nav_dialog"
            android:name="com.sum.navigation.NavDialog"
            android:label="NavActivity"
            tools:layout="@layout/activity_nav" />
</navigation>

该文件中包含着所有节点(目的地),可以在里面指定 Activity,Fragment,Dialog 节点。

  1. 在 MainActivity 中的 xml 文件中添加宿主容器:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/mobile_navigation" />

</FrameLayout>
  • app:defaultNavHost="true":点击返回键的时候主动拦截返回按键,执行页面出栈的操作。
  • app:navGraph="@navigation/mobile_navigation":指定一个 navigation 资源文件,app 中的所有节点全在这个文件当中。
  • app:startDestination:表示 mobile_navigation 资源文件加载完成之后第一次显示的页面,这里是先显示 MainFragment。

内容区使用的是 Fragment 来承载,并且指定了别名 androidx.navigation.fragment.NavHostFragment,它就是一个宿主 Fragment,也就是说主页面的 Fragment 以及其他页面节点(目的地)都将嵌套在 NavHostFragment 下面。在使用的时候必须要通过 navGraph 属性把它和 NavHostFragment 相关联。

  1. 进入 MainActivity 首先加载的是 MainFragment:
class MainFragment : Fragment() {
    private lateinit var binding: FragmentHomeNavBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = FragmentHomeNavBinding.inflate(layoutInflater)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //寻找出控制器对象,它是导航跳转的唯一入口
        val navController = findNavController()
        binding.tvNavActivity.setOnClickListener {
            //导航到Activity(目的地)
            navController.navigate(R.id.nav_activity)
        }
        binding.tvNavFragment.setOnClickListener {
            // 导航到Fragment
            navController.navigate(R.id.nav_fragment)
        }
        binding.tvNavDialog.setOnClickListener {
            // 导航到dialog
            navController.navigate(R.id.nav_dialog)
        }
    }
}

在使用 Navigation 的时候,必须将所有的节点(目的地)添加到 res 文件的 navigation 目录下面的 mobile_navigation.xml 资源文件当中,然后需要在 activity_main.xml 中通过 navGraph 把mobile_navigation.xmlNavHostFragment 关联起来,这才会让宿主把我们定义的节点加载出来。

在这里插入图片描述

  1. NavController 是执行 Navigation 跳转是唯一的入口,通过 navigate 实现导航跳转,可携带参数,指定转场动画。它有多个重载方法:

在这里插入图片描述

val navController: NavController
//点击执行navigate方法跳转到对应id的节点,可以指定Bundle参数。
navController.navigate(int resId, Bundle args, NavOptions navOptions) 
//点击执行navigate方法,会去mobile_navigation.xml中找是否有对应的uri,如果有就会把跳转到该节点上。
navController.navigate(uri Uri)
  1. deepLink 实现页面直达能力
navController.handleDeepLink(Intent())

可以指定一个 uri,当以 uri 的形式来跳转的时候,这个 navigation 会自动从定义的节点当中哪个节点符合条件传递进来的 uri,从而去启动它。

  1. 管理 Fragment 回退栈
navController.navigateUp() //回退到上一个页面
navController.popBackStack(int destinationId, boolean inclusive)
  1. 在节点下面可以添加定义的属性:
  • <action>:定义导航的行为;
  • <argument>:导航节点被创建的时候所需要的参数以及参数的类型;
  • <deepLink>:可以指定一个 uri,当以 uri 的形式来跳转的时候,这个 navigation 会自动从定义的节点当中哪个节点符合传递进来的 uri,从而去启动它。

这是在 mobile_navigation.xml 资源文件当中,去定义一些其他的属性,还有非常多这里不一一列举了。其实这些已经和Android studio 相绑定,点击 Design 进入可视化编辑即可添加,见顶部大图的右侧栏

另外,如果需要实现首页 tab 栏效果,则需要使用 BottomNavigationView 关联起来,同时还需要指定一个 menu 属性,里面定义是按钮 item。具体使用可参考我的 jetpack 实战开源项目:https://github.com/suming77/SumTea_Android

二、Navgation架构概述

导航组件由三个关键部分组成:

  • 导航图:    即 mobile_navigation.xml,在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为目标)以及用户可以通过应用获取的可能路径。
  • NavHost:   显示导航图中目标的空白容器,表示所有节点的宿主。导航组件包含一个默认 NavHost 实现 NavHostFragment),它会显示导航图中的不同目的地。
  • NavController导航控制器,它有着承上启下的作用,会将导航行为委托给它,会通过我们传递的导航视图文件去解析,解析完成之后,就会生成 NavGraph 对象。

Navigation架构图

  • NavgationProvider:导航器 Navgator 管理者,通过它到达每个目的地,实际上就是一个 HashMap。
  • Navigator:  导航器,能够实例化对应的 NavDestination,能指定导航,能回退导航。
  • NavGraph:  它里面存储了所有的导航节点(目的地),也就是存储了所有的页面信息。
  • NavDestination:目的地,表示导航节点,一个个页面。目的地是指您可在应用中导航到的任何位置,通常是 fragment 或 activity。
  • mBackStack: 回退栈管理,每次打开一个页面都会添加一个 NavBackStackEntry

NavHostFragment 表示所有节点的宿主,app:navGraph 允许在 xml 文件中定义导航视图,导航视图里面就定义了一个个的导航节点。

导航时,可以通过页面的 ID 在 NavGraph 查找到目标页的节点,使用 NavController 对象,在导航图中向该对象指示您要去的地方或要使用的路径NavController 随后会在 NavHostFragment显示相应的目的地

三、原理剖析

在使用 Navgation 这个组件的时候,就会使用到 NavHostFragment 因为其他几个 Fragment 都是嵌套在里面,而且 navigation/mobile_navigation 资源文件也传递了进去。

我们先从 NavHostFragment 开始看是如何将 mobile_navigation 这个资源文件是如何被解析生成 navGraph 这个对象的?页面的节点 NavgationDestination 也是如何被创建的?在跳转的时候导航又是如何被执行的?

1. 导航文件解析

通常在自定义 View 的构造函数里面通过 AttributeSet 来解析我们的自定义属性。但是 NavHostFragment 的构造函数里面没有 AttributeSet 这个参数,那么定义在xml的 app:defaultNavHostapp:navGraph 等属性又是如何解析的呢?

确实不是构造方法里面解析的,也不是在 Fragment 的 onCreate 方法中解析的,而是在 NavHostFragmentonInflate() 解析的。

@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
        @Nullable Bundle savedInstanceState) {
    super.onInflate(context, attrs, savedInstanceState);

    final TypedArray navHost = context.obtainStyledAttributes(attrs, R.styleable.NavHost);
    final int graphId = navHost.getResourceId(R.styleable.NavHost_navGraph, 0);
    if (graphId != 0) {
        mGraphId = graphId;
    }
    navHost.recycle();

    final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
    final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
    if (defaultHost) {
        mDefaultNavHost = true;
    }
    a.recycle();
}

这个方法的入参也会有 AttributeSet 这个参数,从而能解析在布局中定义的属性。

任何在布局文件当中声明的组件比如 view,Fragment 当它们在布局当中解析完成,创建成功之后都会回调到 onInflate() 这个方法,但是 Activity 和 Dialog 是没有这个方法的,因为它们还不支持在布局当中声明这两个组件。

当这个两个属性解析完成之后就再从宿主的 onCreate() 方法看起:

public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    final Context context = requireContext();

    // 1.构建NavHostController对象
    mNavController = new NavHostController(context);
    mNavController.setLifecycleOwner(this);
    //2. 设置返回键的Dispatcher,当点击了返回键后将事件分发
    mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
    mNavController.enableOnBackPressed(
            mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
    mNavController.setViewModelStore(getViewModelStore());
    //3.通过NavigatorProvider创建Navigator
    onCreateNavController(mNavController);

    if (mGraphId != 0) {
        // 4.设置从 onInflate() 解析得到的mGraphId
        mNavController.setGraph(mGraphId);
    } else {
        if (graphId != 0) {
            mNavController.setGraph(graphId, startDestinationArgs);
        }
    }
}

这里主要做了四件事:

  1. 首先构建了 NavHostController 对象;
  2. 设置返回键的Dispatcher;
  3. 通过 NavigatorProvider 创建 Navigator
  4. 设置从 onInflate() 解析得到的 mGraphId

构建 NavHostController

首先构建了 NavHostController 对象,实际上 NavHostController 什么都没有,而是继承自 NavController,这样做的目的仅仅是为了和 NavHostFragment 在概念上统一,都是宿主的意思。

public NavController(@NonNull Context context) {
    mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
    mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}

在构造函数里面它注册了两个 Navigator:

  • ActivityNavigator: 就是能够为 Activity 这种组件提供导航服务的 Navigator;
  • NavGraphNavigator: 它是比较特殊的,就是当 mobile_navigation 这个资源文件加载完成之后用来启动 startDestination 的 id 对应的首页,后面还会继续讲到。

之所以在这里实例化注册的原因,是因为一旦确定了 ActivityNavigatorNavGraphNavigatorNavigation 导航器就无法启动 Activity,同时也无法启动导航的首页,Activity 对于一个应用来说是不可或缺的,但是 Fragment 对于所有的应用来说不是必须的。所以 Fragment 类型的 Navigator 并没有在这里注册。那么它是在哪里注册的呢?

设置返回键的Dispatcher

通过 requireActivity().getOnBackPressedDispatcher() 得到一个 OnBackPressedDispatcher,它的作用就是当点击了手机的返回键之后,才能够将事件分发给一个个注册进来的 callback;

@Override
public void setOnBackPressedDispatcher(@NonNull OnBackPressedDispatcher dispatcher) {
    super.setOnBackPressedDispatcher(dispatcher);
}

void setOnBackPressedDispatcher(@NonNull OnBackPressedDispatcher dispatcher) {
    // 将之前注册的移除
    mOnBackPressedCallback.remove();
    // 添加到dispatcher
    dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);
}

拿到 dispatcher 之后调用 addCallback()mOnBackPressedCallback 注册进去,当点击了手机的返回键之后就会回调这个方法 handleOnBackPressed():

private final OnBackPressedCallback mOnBackPressedCallback =
        new OnBackPressedCallback(false) {
    @Override
    public void handleOnBackPressed() {
        popBackStack();
    }
};

popBackStack() 里面 NavGationConttroller 就可以做回退栈的相关操作了,平常如果要监听 Activity 的 onBackPressed()的动作,可以使用 Activity 中的 OnBackPressedDispatcher 向它注册一个回调监听,当点击手机的返回键就会分发到 callback 里面了。

创建 Navigator

protected void onCreateNavController(@NonNull NavController navController) {
   // 注册DialogFragmentNavigator
   navController.getNavigatorProvider().addNavigator(
           new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
   // 注册FragmentNavigator
   navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}

通过 NavController 得到 NavigatorProvider它实际上是存储一个个的 Navigator 对象的,是导航到目的地的有效方法

Fragment 宿主把 DialogFragmentNavigatorFragmentNavigator 注册了,而这两个 Navigator 就是支持 DialogFragment 和 Fragment 页面跳转的,这就是为什么使用 Navigation 导航库的时候使用 NavHostFragment 的原因,否则无法启动 Fragment 的启动和跳转了。

设置 mGraphId

mNavController.setGraph(mGraphId);

把传递进来的导航文件 id 传递了进去,由它去加载这个资源文件,并且生成导航视图 NavGaph 对象。那么就相当 Fragment 宿主,把导航加载以及导航的能力,全部委托给了 NavController,而 NavHostFragment 并不关心导航的存在,起到了隔离的作用

宿主并不需要知道导航的概念,这样设计的好处就是即便换了一个宿主,只需要在一个新的宿主当中创建一个 NavController 就可以完成导航的跳转了。那么导航资源文件如何设置的?

public void setGraph(@NavigationRes int graphResId) {
    setGraph(graphResId, null);
}

public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
    setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
}

getNavInflater().inflate(graphResId) 通过 inflate 方法解析传递进去的节点资源文件:

// 从给出的资源文件id解析出NavGraph
public NavGraph inflate(@NavigationRes int graphResId) {
    Resources res = mContext.getResources();
    XmlResourceParser parser = res.getXml(graphResId);
    final AttributeSet attrs = Xml.asAttributeSet(parser);
    try {
        String rootElement = parser.getName();
        NavDestination destination = inflate(res, parser, attrs, graphResId);
        //······
        return (NavGraph) destination;
    }
}

NavInflater 这个类就是专门用来解析导航图资源文件的,解析完成后返回一个 NavGraph 对象,其实 xml 的解析都是同一个套路,就是一个个去遍历 xml 中的标签,然后和已知的标签去做对比,然后再分门别类去收集去解析,该标签下面的属性,其实和解析自定义属性差不多。

2. 导航节点创建

开启了一个 for 循环,调用 inflate 方法加载 NavDestination,它就是在导航文件中的一个个节点:

private NavDestination inflate(Resources res, XmlResourceParser parser,
AttributeSet attrs, int graphResId)
throws XmlPullParserException, IOException {
    // 1.parser读出标签的名称,然后得到一个Navigator
    Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
    // 2.抽象方法,子类实现
    final NavDestination dest = navigator.createDestination();
    // 3.解析参数
    dest.onInflate(mContext, attrs);

    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
            && ((depth = parser.getDepth()) >= innerDepth
                    || type != XmlPullParser.END_TAG)
    ) {

        // 4.节点的属性
        final String name = parser.getName();
        if (TAG_ARGUMENT.equals(name)) {
            inflateArgumentForDestination(res, dest, attrs, graphResId);
        } else if (TAG_DEEP_LINK.equals(name)) {
            inflateDeepLink(res, dest, attrs);
        } else if (TAG_ACTION.equals(name)) {
            inflateAction(res, dest, attrs, parser, graphResId) {
            } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
                // 5.如果第一个创建的节点是NavGraph,就会递归调用这里的方法,递归调用返回的节点就会被添加到(NavGraph) dest里面
                final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
                final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
                ((NavGraph) dest).addDestination(inflate(id));
                a.recycle();
            } else if (dest instanceof NavGraph) {
                // 6.添加Destination
                ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
            }
        }

        return dest;
    }
}

这里主要做了三件事:

  1. parser 根据名称获取 Navigator,创建 Destination;
  2. dest.onInflate() 解析参数,解析不同类型节点的属性;
  3. 如果第一个创建的节点是 NavGraph,递归调用返回的节点就会被添加到 dest 里面;如果是 NavGraph 则直接添加 Destination

首先parser 解读出标签的名称,而标签就是定义在 mobile_navigation.xml 中的一个个 Fragment 或者 Activity 标签和根节点 navigation 标签。

获取 Navigator 创建 Destination

每个目的地都与一个 Navigator 相关联,该导航器知道如何导航到这个特定的目的地。得到一个 Navigator 对象,如果解析的是 Fragment 标签,那么得到的就是 FragmentNavigator,如果解析的是 Activity 标签得到的就是 ActivityNavigator 标签,然后调用 navigator.createDestination() 方法,它是一个抽象方法,具体的实现在子类里面。这里以 ActivityNavigator 为例:

public Destination createDestination() {
    return new Destination(this);
}

Destination 继承自 NavDestination,构造函数有两个 :

public NavDestination(@NonNull Navigator<? extends NavDestination> navigator) {
    this(NavigatorProvider.getNameForNavigator(navigator.getClass()));
}

public NavDestination(@NonNull String navigatorName) {
    mNavigatorName = navigatorName;
}

构造函数参数不是 navigator 就是 navigatorName ,实际上无论是 Fragment 类型的节点还是 Activty 的节点,在创建的时候都必须把创建这个节点的 navigator 传递过来,从而让 NavDestination 持有 navigatorName ,这样做的目的是为了在跳转的时候能够根据我们指定的目标页的ID,去找到 NavDestination 节点,进而通过 navigatorName 找到创建它的 Navigator,才能够完成正确的跳转。这样就把 NavDestination 和创建它的 navigator 关联了起来。

dest 解析参数和属性

然后通过 dest.onInflate() 解析参数:

private NavDestination inflate(res, parser, attrs, graphResId) {
    //······
    // 3.解释参数,把AttributeSet attrs传递了进去
    dest.onInflate(mContext, attrs);
    //·····
}

public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
    final TypedArray a = context.getResources().obtainAttributes(attrs,
            R.styleable.Navigator);
    setId(a.getResourceId(R.styleable.Navigator_android_id, 0));
    mIdName = getDisplayName(context, mId);
    setLabel(a.getText(R.styleable.Navigator_android_label));
    a.recycle();
}

它里面只解析了必须的参数 id,这个 id 就是导航节点的 id,通常也叫做页面的 id,父类完成后就交由对应的子类去解析对应的属性,
在这里插入图片描述

这里以 ActivityNavigator 为例,看它怎么解析的:

public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
    super.onInflate(context, attrs);
    TypedArray a = context.getResources().obtainAttributes(attrs,
            R.styleable.ActivityNavigator);
    // 1.解析targetPackage
    String targetPackage = a.getString(R.styleable.ActivityNavigator_targetPackage);
    if (targetPackage != null) {
        targetPackage = targetPackage.replace(NavInflater.APPLICATION_ID_PLACEHOLDER,
                context.getPackageName());
    }
    setTargetPackage(targetPackage);
    // 2.解析className
    String className = a.getString(R.styleable.ActivityNavigator_android_name);
    if (className != null) {
        if (className.charAt(0) == '.') {
            className = context.getPackageName() + className;
        }
        setComponentName(new ComponentName(context, className));
    }
    setAction(a.getString(R.styleable.ActivityNavigator_action));
    // 2.解析data数据
    String data = a.getString(R.styleable.ActivityNavigator_data);
    if (data != null) {
        setData(Uri.parse(data));
    }
    setDataPattern(a.getString(R.styleable.ActivityNavigator_dataPattern));
    a.recycle();
}

通过传递进来的 AttributeSet attrs 去解析一个个属性,比如 targetPackage,className,action 等,这些属性解析完成之后都会存储到 Destination 里面,在做导航的时候就能去创建并且启动它了,对于 Fragment 也是同理的。Fragment 更为简单,因为它只需要解析一个 FragmentClassName 就可以了。

NavInflaterinflate() 方法中,由于第一个节点是 navigation (上面的xml文件中可见),所以 parse.getName() 获取到的就是 navigation,而 getNavigator() 得到的就是 NavGraphaNavigator,由它再去创建一个 Destination

//# NavGraphNavigator.class
public NavGraph createDestination() {
    return new NavGraph(this);
}

NavGraph 添加 Destination

将自己传递了进去,同时让 NavGraph 持有自己的名字,NavGraph 同样是 NavDestination 的子类,也就是说他同样是一个节点,但是它是一个特殊的节点,因为它存在一个 mStartDestId,就是在导航当中要启动的那个首页的 mStartDestId,而同样在 onInflate() 当中来解析:

//1.NavGraph也继承自NavDestination,所以说自己也可以嵌套自己的
//即在mobile_navition.xml文件中的Destination节点下嵌套Destination
public class NavGraph extends NavDestination implements Iterable<NavDestination> {
    //存储NavDestination节点,
    final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();
    private int mStartDestId;//要启动的首页id
    private String mStartDestIdName;

    public NavGraph(@NonNull Navigator<? extends NavGraph> navGraphNavigator) {
        super(navGraphNavigator);
    }

    @Override
    public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
        super.onInflate(context, attrs);
        TypedArray a = context.getResources().obtainAttributes(attrs,
                R.styleable.NavGraphNavigator);
        setStartDestination(
                a.getResourceId(R.styleable.NavGraphNavigator_startDestination, 0));
        mStartDestIdName = getDisplayName(context, mStartDestId);
        a.recycle();
    }
}

NavGraph 是一个 NavDestination 节点的集合,可通过 ID 获取。NavGraph 用作“虚拟”目的地:而 NavGraph 本身不会出现在后堆栈上,导航到 NavGraph 将导致目的地将被添加到后堆栈。

<navigation
android:id="@+id/mobile_navigation"
app:startDestination="@id/nav_main">
    //导航节点嵌套
    <navigation>
        <fragment
            android:id="@+id/blankFragment"
            android:name="com.sum.navigation.BlankFragment"
            android:label="fragment_blank"
            tools:layout="@layout/fragment_blank" />
        <fragment
            android:id="@+id/dashboardFragment"
            android:name="com.sum.navigation.DashboardFragment"
            android:label="fragment_dashboard"
            tools:layout="@layout/fragment_dashboard" />
    </navigation>
</navigation>

那么这里就会有导航组的概念,每个组都会有一个首页,通过 startDestination 来指定,那么我们在关闭这个组的时候也就关闭了这个组里面的所有的节点。

private NavDestination inflate(res, parser, attrs, graphResId) {
        //······
        final String name = parser.getName();
        if (TAG_ARGUMENT.equals(name)) {
            inflateArgumentForDestination(res, dest, attrs, graphResId);
        } else if (TAG_DEEP_LINK.equals(name)) {
            inflateDeepLink(res, dest, attrs);
        } else if (TAG_ACTION.equals(name)) {
            inflateAction(res, dest, attrs, parser, graphResId);
        } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
            final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
            final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
            ((NavGraph) dest).addDestination(inflate(id));
            a.recycle();
            //4.如果第一个创建的节点是NavGraph,就会递归调用这里的方法,递归调用返回的节点就会被添加到(NavGraph) dest里面
        } else if (dest instanceof NavGraph) {
            // 5.添加Destination
            ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
        }
    }

    return dest;
}

实际上就是被添加到 mNodes 里面:

public final void addDestination(NavDestination node) {
    //···
    NavDestination existingDestination = mNodes.get(node.getId());
    //···
    node.setParent(this);
    mNodes.put(node.getId(), node);
}

然后这里的导航资源文件就解析完成了:

public NavGraph inflate(@NavigationRes int graphResId) {
    Resources res = mContext.getResources();
    XmlResourceParser parser = res.getXml(graphResId);
    final AttributeSet attrs = Xml.asAttributeSet(parser);
    try {
        String rootElement = parser.getName();
        //1.返回NavDestination
        NavDestination destination = inflate(res, parser, attrs, graphResId);
        //2.如果根节点不是Destination则直接抛出异常
        //返回一个NavGraph
        return (NavGraph) destination;
    }
}

回到 NavController,又调用了内部的 setGraph() 方法:

public void setGraph(NavGraph graph, Bundle startDestinationArgs) {
    mGraph = graph;
    onGraphCreated(startDestinationArgs);
}

这里会把刚刚解析完成的导航图资源文件而生成的 NavGraph 保存起来,然后又调用 onGraphCreated():

private void onGraphCreated(Bundle startDestinationArgs) {
    //···
    if (mGraph != null && mBackStack.isEmpty()) {
        boolean deepLinked = !mDeepLinkHandled && mActivity != null
        && handleDeepLink(mActivity.getIntent());
        if (!deepLinked) {
            //启动第一个导航节点,跳转
            navigate(mGraph, startDestinationArgs, null, null);
        }
    }
}

以上都是导航节点解析和创建的流程。如下图:

NavGraph执行流程图

3. 三种默认类型的导航能力的实现

下面进入导航跳转的流程,在调用 navigate() 的时候把 mGraph 传递了进去:

//虽然使用NavDestination来接收,但是传递进来的实际是NavGraph
private void navigate(NavDestination node, Bundle args,
        NavOptions navOptions, Navigator.Extras navigatorExtras) {
    // 1.通过node.getNavigatorName()找到创建这个节点的Navigator,它实际就是NavGraph对象
    Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
            node.getNavigatorName());
    Bundle finalArgs = node.addInDefaultArgs(args);
    // 2.调用navigate()发起真正的导航
    NavDestination newDest = navigator.navigate(node, finalArgs,
            navOptions, navigatorExtras);
    if (newDest != null) {
        // 3.当执行navigate()后就会把本次的节点添加到回退栈当中
        if (mBackStack.isEmpty()) {
            NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
                    mLifecycleOwner, mViewModel);
            mBackStack.add(entry);
        }
        // 确保所有中间的navgraph都放在回退栈上,确保全局操作工作
        ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
        NavDestination destination = newDest;
        NavBackStackEntry entry = new NavBackStackEntry(mContext, parent, finalArgs,
                        mLifecycleOwner, mViewModel);
                hierarchy.addFirst(entry);
        mBackStack.addAll(hierarchy);
        // 最后,使用它的默认参数添加新的目标
        NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
                newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
        mBackStack.add(newBackStackEntry);
    }
    updateOnBackPressedCallbackEnabled();
    if (popped || newDest != null) {
        dispatchOnDestinationChanged();
    }
}

这里主要做了三件事:

  1. 通过 node.getNavigatorName() 找到创建这个节点的 Navigator,它实际就是 NavGraph 对象;
  2. 调用 navigate() 发起真正的导航;
  3. 当这个导航执行成功之后就会把本次的节点添加到回退栈当中 mBackStack.add(entry),点击了返回键之后就会被 NavController 给拦截了下来,就可以执行真正的回退栈操作了。

进入 NavGraphNavigator#navigate() 看看是如何将首页启动起来的:

//NavGraphNavigator
public NavDestination navigate(NavGraph destination, Bundle args,
 NavOptions navOptions, Extras navigatorExtras) {
    //通过NavGraph找到Destination的id,这个就是导航当中首页要起动的id
    int startId = destination.getStartDestination();
    //通过startId找到这个首页对应的NavDestination
    NavDestination startDestination = destination.findNode(startId, false);
    //通过NavigatorName找到创建这个节点的Navigator
    Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
            startDestination.getNavigatorName());
    return navigator.navigate(startDestination, startDestination.addInDefaultArgs(args),
        navOptions, navigatorExtras);
}

此时这里的 Navigator 就有可能是 AvtivityNavgatiorFragmentNavgatior 以及 DialogNavgatior,也就是说这个 NavGrpahNavgatior 它自己并没有正在执行导航跳转操作,而是把跳转委托给了其他三种 Navgatior 去实现执行,这个类存在的作用就是在 mobile_navigation.xml 资源文件加载完成之后,把首页给启动起来,进入 navigate() 方法。

ActivityNavigator

//#ActivityNavigator.class
public NavDestination navigate(Destination destination, Bundle args,
NavOptions navOptions, Navigator.Extras navigatorExtras) {
    //···
    // 1.将需要传递的参数放入intent
    Intent intent = new Intent(destination.getIntent());
    if (args != null) {
        intent.putExtras(args);
        String dataPattern = destination.getDataPattern();
        data.append(Uri.encode(args.get(argName).toString()));
        matcher.appendTail(data);
        // 用参数填充数据模式,以构建有效的URI
        intent.setData(Uri.parse(data.toString()));
    }
    if (navigatorExtras instanceof Extras) {
        Extras extras = (Extras) navigatorExtras;
        intent.addFlags(extras.getFlags());
    }
    // 2.对请求模式进行判断
    if (!(mContext instanceof Activity)) {
        //如果不是从Activity上下文启动,则必须在一个新任务中启动
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    }
    if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
        intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
    }
    final int destId = destination.getId();
    intent.putExtra(EXTRA_NAV_CURRENT, destId);
    // 3.打开和退出设置动画效果
    if (navOptions != null) {
        // For use in applyPopAnimationsToPendingTransition()
        intent.putExtra(EXTRA_POP_ENTER_ANIM, navOptions.getPopEnterAnim());
        intent.putExtra(EXTRA_POP_EXIT_ANIM, navOptions.getPopExitAnim());
    }
    // 4.最后通过startActivity()来完成Activity类型的导航节点跳转的能力
    if (navigatorExtras instanceof Extras) {
        Extras extras = (Extras) navigatorExtras;
        ActivityOptionsCompat activityOptions = extras.getActivityOptions();
        if (activityOptions != null) {
            ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
        } else {
            mContext.startActivity(intent);
        }
    } else {
        mContext.startActivity(intent);
    }

    //···
    return null;
}

ActivityNavigatornavigate() 将需要传递的参数放入 intent,添加 Data 数据;对请求模式进行判断,设置 flag;设置打开和退出动画效果;最后通过 startActivity() 来完成 Activity 类型的节点的导航能力。

FragmentNavgator

FragmentNavgatornavigate() 的实现:

//#FragmentNavgator.class
public NavDestination navigate(Destination destination, Bundle args,
        NavOptions navOptions, Navigator.Extras navigatorExtras) {
    // 1.通过传递进来的destination得到ClassName,也就是Fragment的全类名
    String className = destination.getClassName();
    if (className.charAt(0) == '.') {
        className = mContext.getPackageName() + className;
    }
    // 2.根据ClassName反射出一个Fragment对象
    final Fragment frag = instantiateFragment(mContext, mFragmentManager,
            className, args);
    // 3.设置参数并开启事务
    frag.setArguments(args);
    final FragmentTransaction ft = mFragmentManager.beginTransaction();
    // 设置进场出场动画效果
    ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
    // 4.通过`replace()`将Fragment添加到容器上面
    ft.replace(mContainerId, frag);
    ft.setPrimaryNavigationFragment(frag);

    final @IdRes int destId = destination.getId();
    final boolean initialNavigation = mBackStack.isEmpty();

    boolean isAdded;
    // 5.添加到后退栈,如果Fragment已经存在后栈中,则替换掉
    mFragmentManager.popBackStack(
                    generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
     ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));

    ft.setReorderingAllowed(true);
    // 6.提交事务
    ft.commit();
    // 提交成功更新视图
    if (isAdded) {
        mBackStack.add(destId);
        return destination;
    } else {
        return null;
    }
}

利用传递进来的 destination 得到 ClassName,反射出一个 Fragment 对象,设置参数并且开启事务,设置进出场动画等, 通过 replace()Fragment 添加到容器上面,同时更新在后退栈中,提交事务。

Fragment 在来回切换的时候都会被频繁销毁重建,重新执行他们的生命周期,如果要避免这种情况可以自定一个 FragmentNavgator,重写 navigate() 方法,使用 hide()show() 实现(开源项目中已解决这个问题)。这就是 FragmentNavgator 实现 Fragment 实现导航节点页面的分析。

DialogFragmentNavigator

下面看下 DialogFragmentNavigatornavigate()

//#DIalogFragmentNavigator.calss
public NavDestination navigate(Destination destination, Bundle args,
NavOptions navOptions, Navigator.Extras navigatorExtras) {
    //1.通过Destination节点得到ClassName,也就是DialogFragment的全类名
    String className = destination.getClassName();
    if (className.charAt(0) == '.') {
        className = mContext.getPackageName() + className;
    }
    //2.通过反射构建一个Fragment对象
    final Fragment frag = mFragmentManager.getFragmentFactory().instantiate(
        mContext.getClassLoader(), className);
    //判断是否为DialogFragment类型
    if (!DialogFragment.class.isAssignableFrom(frag.getClass())) {
        throw new IllegalArgumentException("Dialog destination " + destination.getClassName()
                + " is not an instance of DialogFragment");
    }
    //3.强转并且设置参数和Observer
    final DialogFragment dialogFragment = (DialogFragment) frag;
    dialogFragment.setArguments(args);
    dialogFragment.getLifecycle().addObserver(mObserver);

    //4.显示Dialog
    dialogFragment.show(mFragmentManager, DIALOG_TAG + mDialogCount++);

    return destination;
}

通过 className 反射创建 DialogFragment,强转并且设置参数和 Observer,最后通过调用 show() 显示 Dialog。

四、总结

分析到这里,Navigation 导航库是如何解析导航图文件,以及节点如何被创建,Activity,Fragment,DialogFragment 三种默认类型的导航能力是如何被实现的,相信你已经找到了答案。流程图如下:

在这里插入图片描述

  1. 首先需要一个承载页面的容器 NavHost,这个容器有个默认的实现 NavHostFragment,app:navGraph 加载导航图 xml;

  2. NavHostFragment 有个 NavController 对象,页面导航都是通过调用它的 navigate 方法实现跳转的;

  3. NavController 通过调用 setGraph() 方法,传入导航资源文件,通过 NavInflater 解析导航资源文件,获取导航资源文件中的节点以及属性,得到 NavGraph;

  4. NavController 内部通过 NavigatorProvider 管理这几种 navigator;

  5. NavController 内通过 mBackStack 管理回退栈,设置返回键的 Dispatcher 监听,popBackStack() 就可以做回退栈的相关操作;

  6. NavHostFragment 在 oncreate 方法中,NavController 添加了四个 navigator,分别是FragmentNavigator、ActivityNavigator、DialogFragmentNavigator、NavGraphNavigator,分别实现各自的 navigate 方法,进行页面切换。

  7. 在 navigate 方法中,通过设置参数,action,动画等数据后,根据原生方式实现跳转指定页面,同时会把本次的节点添加到回退栈当中。

优点:

  1. 给 Activity,Fragment,Dialog 提供导航能力的组件。
  2. 导航时可携带参数,指定转场动画。
  3. 支持deepline页面直达,fragment回退栈管理能力。

缺点:

  1. 十分依赖XML文件,所有的节点都必须要在 mobile_navigation.xml文件中来定义,这是不够灵活,不利于模块化,组件化开发。
  2. Fragment 类型的节点来执行导航的时候使用的 replace() 方法会导致页面重新加载重走生命周期方法,不够友好。
  3. 不支持导航过程的拦截和监听。

这是从零到一搭建一个组件化 + 模块化 + 协程 + Flow + Jetpack + MVVM的App,项目地址:https://github.com/suming77/SumTea_Android

点关注,不迷路


好了各位,以上就是这篇文章的全部内容了,很感谢您阅读这篇文章。我是suming,感谢各位的支持和认可,您的点赞就是我创作的最大动力。山水有相逢,我们下篇文章见!

本人水平有限,文章难免会有错误,请批评指正,不胜感激 !

参考链接:

希望我们能成为朋友,在 Github博客 上一起分享知识,一起共勉!Keep Moving!

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。