目录
  1. 1. 前言
  2. 2. 什么是Navigation?
    1. 2.1. Navigation主要元素
  3. 3. Navigation基本使用
    1. 3.1. 添加依赖库
    2. 3.2. 构建路由节点
    3. 3.3. 路由跳转
    4. 3.4. 页面回退
    5. 3.5. deepLink 深度链接
  4. 4. Navigation 架构概述
  5. 5. Navigation路由实现原理
重拾Android-JetPack全家桶(八)之Navigation导航组件

我们知道,单个Activity嵌套多个Fragment的UI架构模式,需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换,这里面还包括AppBar的管理、Fragment间的交替动画、以及Fragment间的参数传递。纯代码的方式在过去大部分开发人员都已经烂熟于心,而且代码编写起来耦合度也比较高。使得使用过程比较混乱。为此,Jetpack提供了一个名为Navigation的组件,旨在方便我们管理页面和AppBar。

前言

一直以来,端内都没有统一的路由组件,在Jetpack的架构下,谷歌为了统一App内所有类型的页面路由规则,推出了Navigation组件。接下来围绕

  • 什么是Navigation?
  • Navigation路由实现原理基本用法
  • Navigation路由实现原理架构介绍
  • Navigation路由实现原理
  • Navigation与ARouter技术选型

这几大方向介绍该组件。

什么是Navigation?

Navigation是端内统一的路由组件,它支持Fragment、Activity、DialogFragment的路由行为;使用Navigation进行页面跳转时可配置动画、参数,也可以自动管理Fragment的回退栈,同时它还支持DeepLink深度链接,页面直达等等。可以说涵盖内容相当完善。它具有以下优势:

  • 可视化的页面导航图,便于页面关系的管理
  • 通过destination的action完成页面间的导航
  • 方便添加页面动画
  • 页面间类型安全的参数传递
  • 通过NavigationUI类,对菜单、底部导航、抽屉菜单导航进行统一的管理
  • 支持深层链接DeepLink
  • Navigation Graph

关联路由资源文件

  • NavHostFragment

承载内容区域的几个fragment的视图

  • NavController

不同fragment间的路由导航控制对象

Navigation基本使用

添加依赖库

使用Navigation组件首先需要添加依赖,如下:

implementation 'androidx.navigation:navigation-fragment:2.3.1'
implementation 'androidx.navigation:navigation-ui:2.3.1'

构建路由节点

App所有的路由页面节点,可以有两种方式生成,一种是直接在创建项目的时候选择 new project --> 选择 Bottom Navigation Activity这个Template模板,它会自动构建出一套基于 navigation 的App模板项目出来;还有一种就是正常建立空项目,手动在资源文件 res --> New --> Android Resource File --> 新建一个Navigation Graph 文件【将File name设置为“nav_graph”,Resource type设置为“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/nav_graph"
app:startDestination="@id/MainFragment">

<fragment
android:id="@+id/MainFragment"
android:name="com.tufusi.autotrack.MainFragment"
android:label="@string/main_fragment_label"
tools:layout="@layout/fragment_main">

<action
android:id="@+id/action_MainFragment_to_FirstFragment"
app:destination="@id/FirstFragment"
app:enterAnim="@anim/slide_right_in"
app:exitAnim="@anim/slide_left_out"
app:popEnterAnim="@anim/slide_left_in"
app:popExitAnim="@anim/slide_right_out" />

<action
android:id="@+id/action_MainFragment_to_FirstFragment"
app:destination="@id/navigation_home_activity" />

<argument
android:name="name"
android:defaultValue="leocheung"
app:argType="string" />

<deepLine
android:id="@+id/deepLink"
app:uri="www.tufusi.com" />

</fragment>

<fragment
android:id="@+id/FirstFragment"
android:name="com.tufusi.autotrack.scenario.AppViewScreenFragment"
android:label="@string/fragment_title1"
tools:layout="@layout/fragment_app_view_screen">

</fragment>

<dialog
android:id="@+id/navigation_home_dialog"
android:name="com.tufusi.autotrack.scenario.DialogFragment">

</dialog>

<activity
android:id="@+id/navigation_home_activity"
android:name="com.tufusi.autotrack.scenario.HomeActivity">

</activity>
</navigation>

app:startDestination:"@id/MainFragment" 声明启动的第一个Fragment页面,navigation标签可以是多级嵌套的,每个navigator标签都必须有app:startDestination属性。

fragment标签用于声明这个路由节点是Fragment类型

action标签必须指定id和destination两个属性,表示跳转的页面路由,也可以配置动画;

argument标签用于页面间参数传递,可以使用插件配置,也可以手动添加该属性;

deepLink标签指深度链接,用于别的页面或者浏览器使用 www.tufusi.com 直接跳转 MainFragment

在主Activity页面的布局,展示如下:

<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />

app:navGraph 必须指定路由导航图

app:defaultNavHost关联实体返回键处理页面回退事件,即该属性设置为true,那么该fragment会自动处理系统返回键。

路由跳转

view.findViewById(R.id.button_first).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 跳转写法一
// NavHostFragment.findNavController(MainFragment.this)
// .navigate(R.id.action_MainFragment_to_FirstFragment);

// 跳转写法二
Navigation.createNavigateOnClickListener(R.id.action_MainFragment_to_FirstFragment);
}
});

跳转有两种写法,任取其中一种,方法一需要获取路由导航控制器,可以通API findNavController获取,导航跳转源码如下:

/**
* Navigate to a destination from the current navigation graph. This supports both navigating
* via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
*
* @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
* navigate to
* @param args arguments to pass to the destination
* @param navOptions special options for this navigation operation
*/
public void navigate(@IdRes int resId, @Nullable Bundle args,
@Nullable NavOptions navOptions) {
navigate(resId, args, navOptions, null);
}

页面回退

指定回退到某个页面,中间打开的页面都会被关闭,类似于启动模式中的栈顶复用模式,inclusive为true时,回退的目标页也会一同被关闭。

public boolean popBackStack(@IdRes int destinationId, boolean inclusive) {
boolean popped = popBackStackInternal(destinationId, inclusive);
// Only return true if the pop succeeded and we've dispatched
// the change to a new destination
return popped && dispatchOnDestinationChanged();
}

回退到上一页

NavHostFragment.findNavController(MainFragment.this).popBackStack();

浏览器或者别的APP在拉起我们的APP时,如果传递的Intent.data为某个页面的deepLink,则会直接路由到该页面:

NavHostFragment.findNavController(MainFragment.this).handleDeepLink(Intent intent)

Navigation 架构概述

主要功能
NavHostFragment 内容区域的宿主container
NavController 作为页面路由导航的控制器
NavGraph 解析xml资源文件生成的对象,手机所有的路由节点Destination
NavgatorProvider 导航器提供者,HashMap存储容器
ActivityNavigator 负责Activity类型的页面跳转以及节点的创建
FragmentNavigator 负责Fragment类型的页面跳转以及节点的创建
DialogFragmentNavigator 负责Dialog类型的页面跳转以及节点的创建
NavGraphNavigator 特殊的导航器,资源文件解析完成后,专门负责首页页面的跳转以及NavGraph节点的创建

架构类图

微信图片_20210118143158.jpg

Navigation路由实现原理

  • 在NavHostFragment宿主容器中构建导航控制器并注册导航器

NavHostFragment.java

public class NavHostFragment extends Fragment implements NavHost {

// 解析布局文件中声明的两个自定义属性:navGraph、defaultNavHost
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
@Nullable Bundle savedInstanceState) {
super.onInflate(context, attrs, savedInstanceState);

final TypedArray navHost = context.obtainStyledAttributes(attrs,
androidx.navigation.R.styleable.NavHost);
final int graphId = navHost.getResourceId(
androidx.navigation.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();
}

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

// NavHostFragment所有关于导航的能力都交托NavController处理,单一职责尽显优势,下面代码中的set功能全部委托控制器管理
mNavController = new NavHostController(context);
// 关联导航控制器与生命周期组件
mNavController.setLifecycleOwner(this);
// 导航控制器设置回退分发器
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());

... ...

// 注册两个导航器
onCreateNavController(mNavController);

... ...

// 委托控制器解析资源文件 - 导航图文件
mNavController.setGraph(mGraphId);
}

protected void onCreateNavController(@NonNull NavController navController) {
// 向NavigatorProvider 注册用于Fragment和DialogFragment路由的导航器
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
}
  • NavController解析路由资源文件生成NavGraph并启动路由首页

NavController.java

public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
// 解析xml文件,并生成NavGraph对象
setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
}

public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) {
// 上一步解析完成,这里保存APP的路由导航结构图
mGraph = graph;
// 通知资源文件解析完成,NavGraph构建完成,打开路由结构图中的第一个页面
onGraphCreated(startDestinationArgs);
}

private void onGraphCreated(@Nullable Bundle startDestinationArgs) {

... ...

if (mGraph != null && mBackStack.isEmpty()) {
boolean deepLinked = !mDeepLinkHandled && mActivity != null
&& handleDeepLink(mActivity.getIntent());
if (!deepLinked) {
// 如果没有深度链接,打开导航结构图中的第一个页面
navigate(mGraph, startDestinationArgs, null, null);
}
} else {
dispatchOnDestinationChanged();
}
}

private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {

... ...

// 通过 node.getNavigatorName() 找到创建它的 navigator来完成导航
// 主要由以下几种
// NavGraphNavigator --> NavGraph
// ActivityNavigator --> ActvityDestination
// FragmentNavigator --> FragmentDestination
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
// 资源文件解析完成后,启动路由的首页,通过 NavGraphNavigator 来导航
// 如果使用 NavController.navigate()来导航,这个navigator就有可能是ActivityNavigator、FragmentNavigator、...
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);

... ...
}
  • NavGraphNavigator启动路由首页

每个导航器都必须标记Navigator.Name注解,用以表明自己是为哪种类页面提供路由服务,同时必须指明该导航器所创建的路由节点的类型。而这也是为什么我们可以在资源文件中使用navigation标签的原因。

NavGraphNavigator.java

@Navigator.Name("navigation")
public class NavGraphNavigator extends Navigator<NavGraph> {

@Override
public NavDestination navigate(@NonNull NavGraph destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Extras navigatorExtras) {
// 找到起始页面的id
int startId = destination.getStartDestination();
if (startId == 0) {
throw new IllegalStateException("no start destination defined via"
+ " app:startDestination for "
+ destination.getDisplayName());
}
// 找到起始页面的 Destination
NavDestination startDestination = destination.findNode(startId, false);
if (startDestination == null) {
final String dest = destination.getStartDestDisplayName();
throw new IllegalArgumentException("navigation destination " + dest
+ " is not a direct child of this NavGraph");
}
// 由于路由的开始页面可能是Fragment,也有可能是Activity,所以首页的具体导航工作还是各个导航器
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
startDestination.getNavigatorName());
return navigator.navigate(startDestination, startDestination.addInDefaultArgs(args),
navOptions, navigatorExtras);
}
}
  • FragmentNavigator的路由跳转实现

这里以Fragment类型的页面作为示例,因为这里有一个问题需要解决,其他类型的页面路由实现几乎类似。

FragmentNavigator.java

// 注解标记必须有,这也是自定义导航器所必须的
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {

// 通过节点中解析到的fragment class 反射实例化出fragment对象
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();

// 配置页面切换动画
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}

// 完成新页面展示工作,可以看到这里使用了 replace
// 这种方式在首页进行页签切换,势必会造成页面重绘,数据重加载,造成资源浪费
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);

final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;

boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size() > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
}
isAdded = false;
} else {
// 加入后退堆栈
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
isAdded = true;
}
if (navigatorExtras instanceof Extras) {
// 配置过渡动画
Extras extras = (Extras) navigatorExtras;
for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
// 最后提交事务完成fragment的路由操作
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
}

到此,Navigation组件功能大致分析完毕。我们可以看出,Navigation在路由导航的时候非常依赖资源文件的配置,但是一般大型APP多则上百个页面,是不可能把所有页面路径配置在资源文件中的,所以相对于组件化项目来说,这还不够灵活,接下来我们可以通过定制优化来改造这一点,使其更实用。

打赏
  • 微信
  • 支付宝

评论