我们知道,单个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主要元素
关联路由资源文件
承载内容区域的几个fragment的视图
不同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) { Navigation.createNavigateOnClickListener(R.id.action_MainFragment_to_FirstFragment); } });
跳转有两种写法,任取其中一种,方法一需要获取路由导航控制器,可以通API findNavController获取,导航跳转源码如下:
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); return popped && dispatchOnDestinationChanged(); }
回退到上一页
NavHostFragment.findNavController(MainFragment.this ).popBackStack();
deepLink 深度链接 浏览器或者别的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节点的创建
架构类图
Navigation路由实现原理
在NavHostFragment宿主容器中构建导航控制器并注册导航器
NavHostFragment.java
public class NavHostFragment extends Fragment implements NavHost { @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(); mNavController = new NavHostController(context); mNavController.setLifecycleOwner(this ); mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher()); ... ... onCreateNavController(mNavController); ... ... mNavController.setGraph(mGraphId); } protected void onCreateNavController (@NonNull NavController navController) { navController.getNavigatorProvider().addNavigator( new DialogFragmentNavigator(requireContext(), getChildFragmentManager())); navController.getNavigatorProvider().addNavigator(createFragmentNavigator()); } }
NavController解析路由资源文件生成NavGraph并启动路由首页
NavController.java
public void setGraph (@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) { setGraph(getNavInflater().inflate(graphResId), startDestinationArgs); } public void setGraph (@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) { mGraph = graph; 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) { ... ... Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator( node.getNavigatorName()); Bundle finalArgs = node.addInDefaultArgs(args); NavDestination newDest = navigator.navigate(node, finalArgs, navOptions, navigatorExtras); ... ... }
每个导航器都必须标记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) { int startId = destination.getStartDestination(); if (startId == 0 ) { throw new IllegalStateException("no start destination defined via" + " app:startDestination for " + destination.getDisplayName()); } 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" ); } Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator( startDestination.getNavigatorName()); return navigator.navigate(startDestination, startDestination.addInDefaultArgs(args), navOptions, navigatorExtras); } }
这里以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) { 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); } ft.replace(mContainerId, frag); ft.setPrimaryNavigationFragment(frag); final @IdRes int destId = destination.getId(); final boolean initialNavigation = mBackStack.isEmpty(); final boolean isSingleTopReplacement = navOptions != null && !initialNavigation && navOptions.shouldLaunchSingleTop() && mBackStack.peekLast() == destId; boolean isAdded; if (initialNavigation) { isAdded = true ; } else if (isSingleTopReplacement) { if (mBackStack.size() > 1 ) { 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()); } } ft.setReorderingAllowed(true ); ft.commit(); if (isAdded) { mBackStack.add(destId); return destination; } else { return null ; } } }
到此,Navigation组件功能大致分析完毕。我们可以看出,Navigation在路由导航的时候非常依赖资源文件的配置,但是一般大型APP多则上百个页面,是不可能把所有页面路径配置在资源文件中的,所以相对于组件化项目来说,这还不够灵活,接下来我们可以通过定制优化来改造这一点,使其更实用。