您现在的位置是:首页 >技术教程 >【见微知著】Android Jetpack - Navigation的架构设计网站首页技术教程
【见微知著】Android Jetpack - Navigation的架构设计
前言:人总是理所当然的忘记,是谁风里雨里,一直默默的守护在原地。
前言
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
:
<?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 节点。
- 在 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
相关联。
- 进入 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)
- 在节点下面可以添加定义的属性:
<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
对象。
- 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<? 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
就可以了。
在 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> {
//存储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);
}
}
}
以上都是导航节点解析和创建的流程。如下图:
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();
}
}
这里主要做了三件事:
- 通过
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<NavDestination> 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,感谢各位的支持和认可,您的点赞就是我创作的最大动力。山水有相逢,我们下篇文章见!
本人水平有限,文章难免会有错误,请批评指正,不胜感激 !