前言
Navigation
库并不像Lifecycle
、LiveData
、ViewModel
能够优雅地解决我们在开发中常遇到的问题。它只是对我们以前在Activity
和Fragment
的界面跳转方式做了统一的封装,可以让开发者减少一些模版代码的编写,提升开发效率。
简单使用
- 添加依赖
implementation 'androidx.navigation:navigation-fragment:2.3.5'
implementation 'androidx.navigation:navigation-ui:2.3.5'
-
创建两个
Fragment
,分别叫做FirstFragment
和SecondFragment
-
在res目录下新建一个名为navigation文件夹,在文件夹内新建一个名为demo_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/first_fragment">
<!-- startDestination表示要加载首页Fragment -->
<fragment
android:id="@+id/first_fragment"
android:name="com.pbl.navigation.navigationdemo.ui.fragment.FirstFragment"
android:label="第一个fragment"
tools:layout="@layout/fragment_first" />
<fragment
android:id="@+id/second_fragment"
android:name="com.pbl.navigation.navigationdemo.ui.fragment.SecondFragment"
android:label="第二个fragment"
tools:layout="@layout/fragment_second" />
</navigation>
- 在
MainActivity
的布局文件中添加承载Fragment
的容器
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--
defaultNavHost表示是否要拦截系统返回键,在Fragment界面,
拦截它是用来处理Fragment回退栈问题
-->
<!--
navGraph给示导航图,也就是我们所有Fragment都会被囊括在这个图内,
这样Navigation框架就可以根据id导航到目标Fragment
-->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/demo_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
从上面可以看出,承载Fragment
的容器是FragmentContainerView
,它是一个FrameLayout
,而它承载的第一个Fragment
是androidx.navigation.fragment.NavHostFragment
。很显然,这是一个占位Fragment
。我们的FirstFragment
在显示的时候肯定会替换它,因此可以推测出会调用Fragment
的replace
方法。
- 在
MainActivity
的onCreate
使用它
// 使用1
NavHostFragment fragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment_activity_main);
// 使用2
NavController controller = fragment.getNavController();
// 使用3
NavigationUI.setupActionBarWithNavController(this, controller);
- 在
FirstFragment
中有一个Button
跳转到SecondFragment
, 它的onClickListener
的实现为:
// 跳转到某个Fragment
Navigation.findNavController(v).navigate(R.id.second_fragment);
// 返回上个Fragment
// Navigation.findNavController(v).navigateUp();
源码分析
使用1处的代码,以前在开发中使用Fragment
时也会遇到,所以对于我们来说是不陌生的。通过xml文件中标明的id
,找到Navigation
框架给我们提供的占位Fragment
: NavHostFragment
。接下来我们就来看看这个Fragment
的生命周期做了一些什么事。
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
final Context context = requireContext();
// 注释1
mNavController = new NavHostController(context);
// Lifecycle相关
mNavController.setLifecycleOwner(this);
// 返回键相关
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
// Set the default state - this will be updated whenever
// onPrimaryNavigationFragmentChanged() is called
mNavController.enableOnBackPressed(
mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
mIsPrimaryBeforeOnCreate = null;
// ViewModel相关
mNavController.setViewModelStore(getViewModelStore());
// 注释2
onCreateNavController(mNavController);
...
// 注释3
if (mGraphId != 0) {
// Set from onInflate()
mNavController.setGraph(mGraphId);
} else {
// See if it was set by NavHostFragment.create()
final Bundle args = getArguments();
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
final Bundle startDestinationArgs = args != null
? args.getBundle(KEY_START_DESTINATION_ARGS)
: null;
if (graphId != 0) {
mNavController.setGraph(graphId, startDestinationArgs);
}
}
super.onCreate(savedInstanceState);
}
先看注释1部分
public NavController(@NonNull Context context) {
mContext = context;
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
mActivity = (Activity) context;
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
NavHostController
继承于NavController
,创建其对象时就做了一件事,添加了两个Navigator
,Navigator
是用来控制界面导航的基类。在这里添加了一个ActivityNavigator
,那肯定还会有一个FragmentNavigator
看看注释2部分
@CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
@Deprecated
@NonNull
protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
return new FragmentNavigator(requireContext(), getChildFragmentManager(),
getContainerId());
}
上面我们猜对了,又添加了两个Navigator
。我们继续看注释3部分。到底是执行if
里面代码还是else里面的代码呢?我们先来看看这个mGraphId
是在哪里赋值的。
@CallSuper
@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();
}
onInflate
方法先于onCreate
执行,它主要做了一件事,获取xml文件中自定义属性。回看上文简单用法的第4步添加的xml内容,有两个自定义的属性,一个是navGraph
,通过这个graphId
就可以获取我们写在res/navigation/demo_navigation.xml里面的内容了。另一个是defaultNavHost
,是否需要拦截系统返回键。
我们再回到注释3,很显然这里会执行if
语句块里面的代码,我们继续跟进:
@CallSuper
public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
}
inflate
方法应该就是去解析我们那个demo_navigation.xml文件,来继续跟踪源码
@SuppressLint("ResourceType")
@NonNull
public NavGraph inflate(@NavigationRes int graphResId) {
Resources res = mContext.getResources();
XmlResourceParser parser = res.getXml(graphResId);
final AttributeSet attrs = Xml.asAttributeSet(parser);
try {
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
// Empty loop
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
String rootElement = parser.getName();
// 真正解析xml文件在这里
NavDestination destination = inflate(res, parser, attrs, graphResId);
if (!(destination instanceof NavGraph)) {
throw new IllegalArgumentException("Root element <" + rootElement + ">"
+ " did not inflate into a NavGraph");
}
return (NavGraph) destination;
} catch (Exception e) {
throw new RuntimeException("Exception inflating "
+ res.getResourceName(graphResId) + " line "
+ parser.getLineNumber(), e);
} finally {
parser.close();
}
}
@NonNull
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
@NonNull AttributeSet attrs, int graphResId)
throws XmlPullParserException, IOException {
// 注释4
Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
final NavDestination dest = navigator.createDestination();
dest.onInflate(mContext, attrs);
final int innerDepth = parser.getDepth() + 1;
int type;
int depth;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlPullParser.END_TAG)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
if (depth > innerDepth) {
continue;
}
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, androidx.navigation.R.styleable.NavInclude);
final int id = a.getResourceId(
androidx.navigation.R.styleable.NavInclude_graph, 0);
((NavGraph) dest).addDestination(inflate(id));
a.recycle();
} else if (dest instanceof NavGraph) {
// 注释5
((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
}
}
return dest;
}
注释4处的navigator
对象到底是指哪一个呢?再回看上文的注释1部分说明,添加的第一个navigator
对象就是NavGraphNavigator
。它在这里做了一件事,通过createDestination
方法创建了一个NavDestination
对象,它是用来干嘛的呢?
根据注释5得知,原来每一个fragment
标签信息都会被封装成一个个NavDestination
对象,这些destination
对象的管者理就是NavGraph
对象。
@CallSuper
public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) {
if (mGraph != null) {
// Pop everything from the old graph off the back stack
popBackStackInternal(mGraph.getId(), true);
}
mGraph = graph;
onGraphCreated(startDestinationArgs);
}
当demo_navigation.xml被解析成一个NavGraph
对象后,通过这个对象跳转到
private void onGraphCreated(@Nullable Bundle startDestinationArgs) {
...
// 首次进来,后退栈mBackStack数据是为空的
if (mGraph != null && mBackStack.isEmpty()) {
// mDeepLinkHandled第一次进来时为false
boolean deepLinked = !mDeepLinkHandled && mActivity != null
&& handleDeepLink(mActivity.getIntent());
if (!deepLinked) {
// Navigate to the first destination in the graph
// if we haven't deep linked to a destination
// 导航到导航图中的第一个界面
navigate(mGraph, startDestinationArgs, null, null);
}
} else {
dispatchOnDestinationChanged();
}
}
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
boolean popped = false;
boolean launchSingleTop = false;
if (navOptions != null) {
if (navOptions.getPopUpTo() != -1) {
popped = popBackStackInternal(navOptions.getPopUpTo(),
navOptions.isPopUpToInclusive());
}
}
// 这里由于是导航到第一个Fragment界面了,所以这个navigator对象是FragmentNavigator
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
...
}
我们继续跟进到FragmentNavigator#navigate
方法中
@Nullable
@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;
}
// 通过反射创建一个Fragment对象
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
// Fragment跳转动画相关
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方法,我们的首页Fragment就在这里替换掉了占位NavHostFragment
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
...
// FragmentTransaction事务提交
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
// 添加到后退栈中
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
分析到这里,我们已经知道了Navigation
框架是如何通过我们配置的demo_navigation.xml文件加载我们的首页Fragment
了。再来看看从第一个Fragment
跳转到第二个Fragment
都做了一些什么?
Navigation.findNavController(root).navigate(R.id.second_fragment);
先找到NavController
对象,从这里可以看出,导航的总控制器是这个对象。继续跟进这个navigate
方法,最终会执行下面这个方法
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
// 因为我们已经加载了第一个FirstFragment了,所以此时mBackStack不为空,
// 那么currentNode就是FirstFragment对应的NavDestination
NavDestination currentNode = mBackStack.isEmpty()
? mGraph
: mBackStack.getLast().getDestination();
if (currentNode == null) {
throw new IllegalStateException("no current navigation node");
}
@IdRes int destId = resId;
final NavAction navAction = currentNode.getAction(resId);
Bundle combinedArgs = null;
if (navAction != null) {
if (navOptions == null) {
navOptions = navAction.getNavOptions();
}
destId = navAction.getDestinationId();
Bundle navActionArgs = navAction.getDefaultArguments();
if (navActionArgs != null) {
combinedArgs = new Bundle();
combinedArgs.putAll(navActionArgs);
}
}
if (args != null) {
if (combinedArgs == null) {
combinedArgs = new Bundle();
}
combinedArgs.putAll(args);
}
if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
return;
}
if (destId == 0) {
throw new IllegalArgumentException("Destination id == 0 can only be used"
+ " in conjunction with a valid navOptions.popUpTo");
}
// 通过我们传入的R.id.second_fragment这个ID值去查找对应的NavDestination
// 去哪里查找?上文有提到过所有界面的NavDestination都是由NavGraph对象管理的
NavDestination node = findDestination(destId);
if (node == null) {
final String dest = NavDestination.getDisplayName(mContext, destId);
if (navAction != null) {
throw new IllegalArgumentException("Navigation destination " + dest
+ " referenced from action "
+ NavDestination.getDisplayName(mContext, resId)
+ " cannot be found from the current destination " + currentNode);
} else {
throw new IllegalArgumentException("Navigation action/destination " + dest
+ " cannot be found from the current destination " + currentNode);
}
}
// 这里就会执行到我们上文分析过的navigate方法了
navigate(node, combinedArgs, navOptions, navigatorExtras);
}
源码差不多分析完了,最后用一张类UML图来总结一相涉及到的相关类。
navigation.png
从类的UML图可以看出,Navigation
框架使用了设计模式中的策略模式,Navigator
定义了一个导航规则,具体怎么导航由子类来实现。这里的子类就是图中的ActivityNavigator
、FragmentNavigator
等。
遇到的问题
- 通过上面源码分析,
Fragment
的加载都是通过replace
方法,也就意味着每次回退时界面都会重新渲染,很显然这不符合我们的开发需求 -
Fragment
只能在xml文件配置,若是分模块开发,无法满足业务需求
下篇文章将会介绍如何优雅地解决上面的两个问题。
网友评论