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

前言

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

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

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

一、基本使用

在 build.gradle 文件中添加依赖,目前版本是 2.5.3

implementation 'androidx.navigation:navigation-fragment:$version'

implementation 'androidx.navigation:navigation-ui:$version'

在 res 文件夹下新建一个 navigaton 文件夹,创建导航图 mobile_navigation.xml:

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">

android:id="@+id/nav_main"

android:name="com.sum.navigation.MainFragment"

android:label="HomeNavFragment"

tools:layout="@layout/fragment_home_nav" />

android:id="@+id/nav_activity"

android:name="com.sum.navigation.NavActivity"

android:label="NavActivity"

tools:layout="@layout/activity_nav" />

android:id="@+id/nav_fragment"

android:name="com.sum.navigation.NavFragment"

android:label="FindNavFragment"

tools:layout="@layout/fragment_home_nav" />

android:id="@+id/nav_dialog"

android:name="com.sum.navigation.NavDialog"

android:label="NavActivity"

tools:layout="@layout/activity_nav" />

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

在 MainActivity 中的 xml 文件中添加宿主容器:

xmlns:app="http://schemas.android.com/apk/res-auto"

android:layout_width="match_parent"

android:layout_height="match_parent">

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" />

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 相关联。

进入 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.xml 和 NavHostFragment 关联起来,这才会让宿主把我们定义的节点加载出来。

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)

deepLink 实现页面直达能力

navController.handleDeepLink(Intent())

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

管理 Fragment 回退栈

navController.navigateUp() //回退到上一个页面

navController.popBackStack(int destinationId, boolean inclusive)

在节点下面可以添加定义的属性:

:定义导航的行为;:导航节点被创建的时候所需要的参数以及参数的类型;:可以指定一个 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 对象。

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:defaultNavHost 和 app:navGraph 等属性又是如何解析的呢?

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

@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);

}

}

}

这里主要做了四件事:

首先构建了 NavHostController 对象;设置返回键的Dispatcher;通过 NavigatorProvider 创建 Navigator;设置从 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 对应的首页,后面还会继续讲到。

之所以在这里实例化注册的原因,是因为一旦确定了 ActivityNavigator 和 NavGraphNavigator,Navigation 导航器就无法启动 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 宿主把 DialogFragmentNavigator 和 FragmentNavigator 注册了,而这两个 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;

}

}

这里主要做了三件事:

parser 根据名称获取 Navigator,创建 Destination;dest.onInflate() 解析参数,解析不同类型节点的属性;如果第一个创建的节点是 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 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 就可以了。

在 NavInflater 的 inflate() 方法中,由于第一个节点是 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节点,

final SparseArrayCompat mNodes = new SparseArrayCompat<>();

private int mStartDestId;//要启动的首页id

private String mStartDestIdName;

public NavGraph(@NonNull Navigator 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 将导致目的地将被添加到后堆栈。

android:id="@+id/mobile_navigation"

app:startDestination="@id/nav_main">

//导航节点嵌套

android:id="@+id/blankFragment"

android:name="com.sum.navigation.BlankFragment"

android:label="fragment_blank"

tools:layout="@layout/fragment_blank" />

android:id="@+id/dashboardFragment"

android:name="com.sum.navigation.DashboardFragment"

android:label="fragment_dashboard"

tools:layout="@layout/fragment_dashboard" />

那么这里就会有导航组的概念,每个组都会有一个首页,通过 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);

}

}

}

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

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

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

//虽然使用NavDestination来接收,但是传递进来的实际是NavGraph

private void navigate(NavDestination node, Bundle args,

NavOptions navOptions, Navigator.Extras navigatorExtras) {

// 1.通过node.getNavigatorName()找到创建这个节点的Navigator,它实际就是NavGraph对象

Navigator 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 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();

}

}

这里主要做了三件事:

通过 node.getNavigatorName() 找到创建这个节点的 Navigator,它实际就是 NavGraph 对象;调用 navigate() 发起真正的导航;当这个导航执行成功之后就会把本次的节点添加到回退栈当中 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 navigator = mNavigatorProvider.getNavigator(

startDestination.getNavigatorName());

return navigator.navigate(startDestination, startDestination.addInDefaultArgs(args),

navOptions, navigatorExtras);

}

此时这里的 Navigator 就有可能是 AvtivityNavgatior、FragmentNavgatior 以及 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;

}

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

FragmentNavgator

FragmentNavgator 的 navigate() 的实现:

//#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

下面看下 DialogFragmentNavigator 的 navigate():

//#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 三种默认类型的导航能力是如何被实现的,相信你已经找到了答案。流程图如下:

首先需要一个承载页面的容器 NavHost,这个容器有个默认的实现 NavHostFragment,app:navGraph 加载导航图 xml; NavHostFragment 有个 NavController 对象,页面导航都是通过调用它的 navigate 方法实现跳转的; NavController 通过调用 setGraph() 方法,传入导航资源文件,通过 NavInflater 解析导航资源文件,获取导航资源文件中的节点以及属性,得到 NavGraph; NavController 内部通过 NavigatorProvider 管理这几种 navigator; NavController 内通过 mBackStack 管理回退栈,设置返回键的 Dispatcher 监听,popBackStack() 就可以做回退栈的相关操作; NavHostFragment 在 oncreate 方法中,NavController 添加了四个 navigator,分别是FragmentNavigator、ActivityNavigator、DialogFragmentNavigator、NavGraphNavigator,分别实现各自的 navigate 方法,进行页面切换。 在 navigate 方法中,通过设置参数,action,动画等数据后,根据原生方式实现跳转指定页面,同时会把本次的节点添加到回退栈当中。

优点:

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

缺点:

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

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

点关注,不迷路

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

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

参考链接:

Navigation官网Jetpack 导航

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

相关链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: