本文是Navigation分析文章的下篇,内容续Jetpack 源码分析(七) - 手把手教你认识Navigation(上)。在阅读本文之前,推荐优先看上篇的内容,方便很多的知识点能够串联起来。本文的主要内容是:
- 跳转逻辑的实现。
- FragmentNavigator的分析。
- 如何自定义Navigator,如何自定义传参?
- Navigation的一些设计美学和"缺点"。
本文参考文章:
本文demo代码:NavigationDemo。
1. 跳转逻辑的实现
在上篇内容中,我们已经知道页面可以使用NavController的navigate方法来实现跳转,同时在前面也简单介绍过跳转的流程。但是,前面介绍的知识过于简单和笼统,其内部的实现原理并没有过多的介绍。因此,在这里,我们深入源码中去介绍其的实现原理。
本节主要内容是:
- 初始化的跳转,即graph的startDestination。
- action的跳转。
- deeplink跳转。
- popUpTo 和 popUpToInclusive的特别分析。
(1). 初始化跳转
我们在上篇内容中已经说过,当NavController在inflate graph的时候,其实还做了一件事,那就是需要跳转到该graph使用startDestination
标记的页面。那么这个初始化跳转做了哪些事呢?我们应该需要注意哪些问题呢?我们直接来看代码,首先看NavController
的onGraphCreated
方法:
private void onGraphCreated(@Nullable Bundle startDestinationArgs) {
// 省略状态恢复的代码
if (mGraph != null && mBackStack.isEmpty()) {
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();
}
}
这个方法里面的代码主要分为两个部分:
- 如果当前的返回栈是空的,那么就调用
navigate
方法跳转到默认页面上去。- 如果返回栈不为空,那么调用
dispatchOnDestinationChanged
方法去更新相关的信息。主要是两个方面的信息:首先就把当前返回栈顶的所有的NavGraph都给弹出;其次就是,是更新NavBackStackEntry
的生命周期,注意,每一个NavDestination
在返回栈都会被NavBackStackEntry
包裹。
这里需要注意的是只有在不处理deepLink的时候才会跳转到默认页面上去。那么handleDeepLink
方法里面是怎么处理deepLink的呢?我们先来看代码:
public boolean handleDeepLink(@Nullable Intent intent) {
if (intent == null) {
return false;
}
// 1. 从Intent中去解析DeepLink。
Bundle extras = intent.getExtras();
int[] deepLink = extras != null ? extras.getIntArray(KEY_DEEP_LINK_IDS) : null;
Bundle bundle = new Bundle();
Bundle deepLinkExtras = extras != null ? extras.getBundle(KEY_DEEP_LINK_EXTRAS) : null;
if (deepLinkExtras != null) {
bundle.putAll(deepLinkExtras);
}
if ((deepLink == null || deepLink.length == 0) && intent.getData() != null) {
NavDestination.DeepLinkMatch matchingDeepLink =
mGraph.matchDeepLink(new NavDeepLinkRequest(intent));
if (matchingDeepLink != null) {
NavDestination destination = matchingDeepLink.getDestination();
deepLink = destination.buildDeepLinkIds();
Bundle destinationArgs =
destination.addInDefaultArgs(matchingDeepLink.getMatchingArgs());
bundle.putAll(destinationArgs);
}
}
// 2. 验证DeepLink的合法性
if (deepLink == null || deepLink.length == 0) {
return false;
}
String invalidDestinationDisplayName =
findInvalidDestinationDisplayNameInDeepLink(deepLink);
if (invalidDestinationDisplayName != null) {
Log.i(TAG, "Could not find destination " + invalidDestinationDisplayName
+ " in the navigation graph, ignoring the deep link from " + intent);
return false;
}
bundle.putParcelable(KEY_DEEP_LINK_INTENT, intent);
int flags = intent.getFlags();
// 3. 启动对应的Activity。
if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0
&& (flags & Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0) {
// Someone called us with NEW_TASK, but we don't know what state our whole
// task stack is in, so we need to manually restart the whole stack to
// ensure we're in a predictably good state.
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
TaskStackBuilder taskStackBuilder = TaskStackBuilder
.create(mContext)
.addNextIntentWithParentStack(intent);
taskStackBuilder.startActivities();
if (mActivity != null) {
mActivity.finish();
// Disable second animation in case where the Activity is created twice.
mActivity.overridePendingTransition(0, 0);
}
return true;
}
// 4. 不启动新的Activity,默认导航到对应的页面。
if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
// Start with a cleared task starting at our root when we're on our own task
if (!mBackStack.isEmpty()) {
popBackStackInternal(mGraph.getId(), true);
}
int index = 0;
while (index < deepLink.length) {
int destinationId = deepLink[index++];
NavDestination node = findDestination(destinationId);
if (node == null) {
final String dest = NavDestination.getDisplayName(mContext, destinationId);
throw new IllegalStateException("Deep Linking failed:"
+ " destination " + dest
+ " cannot be found from the current destination "
+ getCurrentDestination());
}
navigate(node, bundle,
new NavOptions.Builder().setEnterAnim(0).setExitAnim(0).build(), null);
}
return true;
}
// 5. 没有FLAG_ACTIVITY_NEW_TASK的表示,那就不用清空堆栈,直接导航。
// Assume we're on another apps' task and only start the final destination
NavGraph graph = mGraph;
for (int i = 0; i < deepLink.length; i++) {
int destinationId = deepLink[i];
NavDestination node = i == 0 ? mGraph : graph.findNode(destinationId);
if (node == null) {
final String dest = NavDestination.getDisplayName(mContext, destinationId);
throw new IllegalStateException("Deep Linking failed:"
+ " destination " + dest
+ " cannot be found in graph " + graph);
}
if (i != deepLink.length - 1) {
// We're not at the final NavDestination yet, so keep going through the chain
graph = (NavGraph) node;
// Automatically go down the navigation graph when
// the start destination is also a NavGraph
while (graph.findNode(graph.getStartDestination()) instanceof NavGraph) {
graph = (NavGraph) graph.findNode(graph.getStartDestination());
}
} else {
// Navigate to the last NavDestination, clearing any existing destinations
navigate(node, node.addInDefaultArgs(bundle), new NavOptions.Builder()
.setPopUpTo(mGraph.getId(), true)
.setEnterAnim(0).setExitAnim(0).build(), null);
}
}
mDeepLinkHandled = true;
return true;
}
针对于这段代码,主要做了如下几件事:
- 从Intent获取对应的DeepLink,如果Intent没有直接携带DeepLink的信息,那就根据从Intent的Data信息,尝试解析出对应的DeepLink。
- 验证DeepLink的合法性。首先是看解析出来的DeepLink是否为空,如果不为空的话,需要看一下每个Destination的id name是否合法。id name 是否合法,主要是根据id对应的int值去Resource去寻找对应的name,如果能够找到就表示合法的。
- 如果Intent的Flag表示需要新启Task,且不用清空任务栈,直接新启Activity。
- 如果Intent的Flag表示需要新启Task,且需要清空任务栈,不用新启Activity,直接clear掉当前返回栈中不合理的页面,然后导航到DeepLink的页面。
- 如果不用新启Task,就直接导航到DeepLink的页面。
所做的事情有点多,但是我们只需要重点关注第5点,因为这种情况下,mDeepLinkHandled
才会设置为true。且需要特别补充的是,本文主要介绍的是单Activity+多Fragment的解决方案,涉及跨Activity的情况可以先忽略,有兴趣的同学可以自行阅读源码理解。
一般来说,我们不会在Intent里面塞这些数据,因此初次进入页面,不会通过DeepLink启动页面,也就是说,都会使用startDestination来初始化跳转。我们直接来看navigate方法,看一下如何实现跳转的。
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());
}
}
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
if (newDest != null) {
// ......
// 将新的页面包装成为一个NavBackStackEntry,放到返回栈中。如果目标页面是一个NavGraph,可能会向返回栈中放入多个NavBackStackEntry实例
} else if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
// 如果目标页面采用的是SingleTop方式启动,且栈顶页面是目标,那么直接更新栈顶NavBackStackEntry的信息
launchSingleTop = true;
NavBackStackEntry singleTopBackStackEntry = mBackStack.peekLast();
if (singleTopBackStackEntry != null) {
singleTopBackStackEntry.replaceArguments(finalArgs);
}
}
updateOnBackPressedCallbackEnabled();
if (popped || newDest != null || launchSingleTop) {
dispatchOnDestinationChanged();
}
}
navigate方法实现导航逻辑,主要分为如下三步:
- 根据NavDestination的NavigatorName获取对应的Navigator对象,而NavigatorName就是每个Navigator类前面的
Navigator.Name
注解,比如如下:
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
这里我们的NavDestination是NavGraph,拿到的自然是NavGraphNavigator对象。
- 拿到Navigator对象之后,就是调用其navigate方法,实现页面跳转的逻辑。此方法会返回的是一个新的NavDestination对象,如果返回值不为空,表示跳转到新的页面;如果返回值为空,表示没有跳转到新的页面。
- 在第二步中拿到返回值之后,如果返回值不为空,那么就会将新的页面包装成为NavBackStackEntry对象放到返回栈中。这块逻辑听上去比较简单,实际上还做了很多的其他的事情,比如说目标页面又是一个
NavGraph
等。不过这里我们只考虑正常的跳转,也就是说就只有一个页面进入返回栈,这样理解起来比较简单。如果返回值为空,那么就要看看页面启动方式是否是SingleTop
,如果是,就需要更新栈顶NavBackStackEntry
的信息。
接下来,我们来看看NavGraphNavigator
的navigate
方法是怎么实现初始化跳转的。
@Nullable
@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);
}
这个方法实现很简单,基本逻辑就是拿到NavGraph中的StartDestination,然后跳转,跳转方式跟NavController
内部方式基本类似。其中,需要注意的是:
- 这里的
StartDestination
就是我们在graph文件里面定义的startDestination属性。- 这里拿到的
Navigator
可能是NavGraphNavigator
、FragmentNavigator
,甚至是ActivityNavigator
,具体需要看我们在graph文件中定义的是什么。
这里我们就只考虑FragmentNavigator
,其他的暂且不分析。同时,后续我们会章节专门介绍FragmentNavigator
,这里就不对FragmentNavigator
进行展开了。
(2).action跳转
一般来说,我们使用Action跳转,都是直接传递一个action id,例如下面的写法:
findNavController().navigate(R.id.action_to_child_a)
一般来说,这个方法都调用到如下方法里面去:
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
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");
}
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(node, combinedArgs, navOptions, navigatorExtras);
}
这个方法的代码有点长,但是实际上所做的事情很简单:
- 根据传递的action id,在当前NavDestination中寻找对应的NavAction。从这里我们可以知道,为啥局部action只能当前页面自己使用了,如果这里使用的是其他页面的局部action,这里就会找不到对应的NavAction。
- 找到NavAction之后,就能拿到跳转页面的NavDestination,进而进行跳转。这里的跳转逻辑跟之前初始化跳转的是一样的。这里就不展开分析了。
这里面有一点可能需要注意的是:
if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
return;
}
这段代码表达的意思是,如果action中配置了app:popUpTo
属性,就不会发生跳转行为,只会发生返回行为。这一块代码在分析popTo
的时候会重点分析。
(3). deeplink跳转
我们一般都是通过Uri来实现DeepLink的跳转,如下:
addViewWithClickListener("使用deepLink跳转到NavChildFragmentB") {
findNavController().navigate("https://www.jade.com".toUri())
}
而NavController内部会Uri包装成为一个NavDeepLinkRequest
对象,然后调用到如下代码处:
public void navigate(@NonNull NavDeepLinkRequest request, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
NavDestination.DeepLinkMatch deepLinkMatch =
mGraph.matchDeepLink(request);
if (deepLinkMatch != null) {
NavDestination destination = deepLinkMatch.getDestination();
Bundle args = destination.addInDefaultArgs(deepLinkMatch.getMatchingArgs());
if (args == null) {
args = new Bundle();
}
NavDestination node = deepLinkMatch.getDestination();
Intent intent = new Intent();
intent.setDataAndType(request.getUri(), request.getMimeType());
intent.setAction(request.getAction());
args.putParcelable(KEY_DEEP_LINK_INTENT, intent);
navigate(node, args, navOptions, navigatorExtras);
} else {
throw new IllegalArgumentException("Navigation destination that matches request "
+ request + " cannot be found in the navigation graph " + mGraph);
}
}
这个方法的基本逻辑是通过封装好的NavDeepLinkRequest
,在Graph
中寻找对应的页面,然后进行跳转。如果找不到的话,那表示是一个非法的Uri,就会直接崩溃。
而通过NavDeepLinkRequest
寻找对应页面的过程,就是一个正则表达式匹配的过程,我们来看一下NavGraph
的matchDeepLink
方法:
DeepLinkMatch matchDeepLink(@NonNull NavDeepLinkRequest request) {
// First search through any deep links directly added to this NavGraph
DeepLinkMatch bestMatch = super.matchDeepLink(request);
// Then search through all child destinations for a matching deep link
for (NavDestination child : this) {
DeepLinkMatch childBestMatch = child.matchDeepLink(request);
if (childBestMatch != null && (bestMatch == null
|| childBestMatch.compareTo(bestMatch) > 0)) {
bestMatch = childBestMatch;
}
}
return bestMatch;
}
这个方法里面主要做了两件事:
- 调用super的
matchDeepLink
方法,进行正则匹配,去寻找合适的页面。- 无论第一步是否找到对应页面,都要去
NavGraph
的child里面去寻找合适的页面,且最终的返回的结果是匹配度最高的页面。
找到对应的页面之后,就正常调用对应的navigate方法进行跳转。这里就不重复介绍了。
(3). popUpTo 和 popUpToInclusive
需要提前说明的时候,popUpTo
本质上不是一种跳转方式,它依赖于action跳转。它所表示的含义是,在action跳转过程中所做一些事情。
popUpTo主要分为两种情况:
- 配置了popUpTo属性,且在跳转过程中,返回栈已有对应页面的实例。此时会清空此页面之上的所有实例,而是否清空自己由
popUpToInclusive
控制。然后创建此页面的新实例放入返回栈。- 配置了popUpTo属性,且在跳转过程中,返回栈没有对应页面的实例,那就跟正常跳转是一样的。
这里需要特别说明的第一点,是否创建一个新实例是不完全正确的,还得看action的配置,如果我们这么配置,就会直接返回,不会创建新实例:
<action
android:id="@+id/action_child_b_to_a_by_popUp"
app:popUpTo="@id/fragment_nav_child_a"
app:popUpToInclusive="false" />
相比于其他的action,此action少了一个destination
属性。这块代码实现在上面action已经特别说明了。就是如下:
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
// ......
if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
return;
}
// ......
}
如果配置了popUpTo属性,且没有设置destination,这里就会直接返回到对应页面。同时需要注意的是,如果这时候返回栈中没有对应页面的实例,那么就会直接返回到graph对应的初始化页面。
2. FragmentNavigator分析
前面在分析跳转的时候,已经提到过了Navigator。页面的跳转主要是依赖Navigator的navigate方法来实现的,但是Navigator的作用不仅仅是用来实现页面跳转的,它的作用可以定义为维护页面,包括页面跳转和页面返回。
本小节主要内容是:
- 页面跳转逻辑的实现
- 页面返回逻辑的实现。特别区分
popBackStack
和navigateUp
(1). 页面跳转逻辑的实现
我们直接看FragmentNavigator
的navigate
方法:
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
// 1. 创建对应的页面对象。
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);
}
// 2. 展示页面且结果back事件的处理。
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;
// 3. 将Fragment添加到FragmentManager的返回栈里面去
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());
}
}
ft.setReorderingAllowed(true);
ft.commit();
// 4. 如果导航到一个新的页面,那么就会返回新页面的实例;否则就返回为null.
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
navigate方法主要是做了如下几件事:
- 首先是创建Fragment的对象。这里不得不说几句,Navigation默认创建Fragment创建对象,我们想要在给Fragment传递一些参数都非常困难,同时创建方式还是默认的,不能自定义。
- 使用replace方式展示页面。使用replace的方式需要的注意的是,上一个页面会onDestroyView,这也是前面说的onCreateView可能会重复调用的原因所在。
- 将对应页面添加到返回栈中。这一步主要是Fragment的入场动画生效,要想一个Fragment入场动画生效,必须将其Fragment添加到FragmentManager的返回栈,也是调用FragmentManager的addToBackStack方法。需要注意的是,慎用
addToBackStack
方法,因为在我实际开发过程中发现,如果一个Fragment被添加到FragmentManager的返回栈中,此时直接remove此Fragment,Fragment的生命周期只会走到onDestroyView,不会到onDestroy,这是因为FragmentManager在处理生命周期的时候,会判断此Fragment是否在返回栈中。所以,如果我们页面栈中有很多的Fragment,这些Fragment都在FragmentManager的返回栈中,如果想要移除页面栈中某个页面,又不想remove此页面之上的页面,此时Fragment生命周期就会出问题。进而,如果使用了addToBackStack
方法,一定要使用popBackStack
方法弹出此Fragment。- 最后就是返回新页面的实例。
从FragmentNavigator
中可以看出来,整个Navigation导航过程中,自定义了两个返回栈,一个是FragmentNavigator的返回栈,一个NavController的返回栈,这两个返回栈push操作还是分开的,这个无疑是增加整个框架的复杂度。为什么这么说呢?从代码实现上来看,FragmentNavigator
入栈了一个新页面,NavController
入栈的页面可能不止一个,这一点会使理解门槛变高了,同时维护成本也变大了。
(2). 页面返回逻辑的实现
在FragmentNavigator,页面返回的实现逻辑主要体现在popBackStack
方法里面:
public boolean popBackStack() {
if (mBackStack.isEmpty()) {
return false;
}
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already"
+ " saved its state");
return false;
}
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
mBackStack.removeLast();
return true;
}
因为FragmentNavigator默认将每个Fragment添加到FragmentManager的返回栈,所以直接使用popBackStack
方法弹出栈顶的页面。
这里想要特别说明,popBackStack
方法如果弹出的是返回栈中间的页面,那么此页面和其之上的页面都会被弹出。这也是我想吐槽的地方,目前FragmentManager不支持随意删除返回栈的元素,有点鸡肋。算了算了,大家还是慎用addToBackStack
和popBackStack
方法,最好是自定义Navigator。
在NavController
中,有两个方法都可以完成返回逻辑,那就是popBackStack
和navigateUp
。这里就不对两个方法进行展开,我就简单说一下这两个的区别:
popBackStack:如果返回栈不为空,会一个一个退出页面栈的页面;如果返回栈为空,不做任何处理。
navigateUp: 如果当前返回栈中只有一个页面,会把当前Activity给finish;如果页面栈中有多个页面,会调用popBackStack方法执行返回逻辑。
这就是这两个方法最大的差别,大家可以根据自身的场景,来决定调用哪个方法。详细的区别可以参考对应的源码实现和Difference between navigateUp() and popBackStack()。
3. 自定义Navigator
既然Google爸爸提供的FragmentNavigator不合我们的心意,那么我们可以自定义自己的Navigator,那么怎么自定义Navigator呢?本小节来重点介绍,主要内容如下:
- 自定义Navigator,正确维护Fragment的生命周期。
- 自定义传参。
(1). 自定义Navigator
自定义Navigator,可以自行选择Navigator
作为父类,还是FragmentNavigator
作为父类。这里我以Navigator
作为父类,实现代码如下:
@Navigator.Name("KeepStateFragment")
class KeepStateNavigator(
private val mContext: Context,
private val mFragmentManager: FragmentManager,
private val mContainerId: Int
) : Navigator<FragmentNavigator.Destination>() {
private val mBackStack: Stack<Fragment> = Stack()
override fun createDestination(): FragmentNavigator.Destination {
return FragmentNavigator.Destination(this)
}
override fun navigate(
destination: FragmentNavigator.Destination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Extras?
): NavDestination {
return mFragmentManager.beginTransaction().run {
var className = destination.className
if (className[0] == '.') {
className = mContext.packageName + className
}
val frag = instantiateFragment(
mContext, mFragmentManager,
className, args
)
getCurrentFragment()?.let {
setMaxLifecycle(it, Lifecycle.State.STARTED)
}
frag.arguments = args
add(mContainerId, frag)
mBackStack.add(frag)
commitAllowingStateLoss()
destination
}
}
override fun popBackStack(): Boolean {
if (mBackStack.isEmpty()) {
return false
}
mFragmentManager.beginTransaction().apply {
val popFragment = mBackStack.pop()
remove(popFragment)
getCurrentFragment()?.run {
setMaxLifecycle(this, Lifecycle.State.RESUMED)
}
commitAllowingStateLoss()
}
return true
}
private fun getCurrentFragment(): Fragment? {
return if (mBackStack.isEmpty()) null else mBackStack.peek()
}
fun instantiateFragment(
context: Context,
fragmentManager: FragmentManager,
className: String, args: Bundle?
): Fragment {
return fragmentManager.fragmentFactory.instantiate(
context.classLoader, className
)
}
}
如上代码注意几点:
KeepStateNavigator
前面需要加上Navigator.Name
注解,注解设置的名称就是该Navigator能够处理的Destination,这个会在graph文件使用得到。- 重写
createDestination
、navigate
和popBackStack
方法。其中navigate方法就是一个新的页面入栈,popBackStack
方法就是栈顶页面出栈。
从KeepStateNavigator
实现上来看,我们使用的是add方式添加Fragment,跟原生的replace方式是不一样;其次,为了保证Fragment生命周期的正确性,我们在新页面入栈的时候,会通过setMaxLifecycle
方法把旧页面的生命周期设置到STARTED
,在栈顶页面出栈的时候,会把新的栈顶页面生命周期设置到RESUMED
。这个操作只能让Fragment在onPause和onResume之间转化,这也是我们能做到的最大限度了,一般来说已经能够覆盖很多业务场景。PS:如果Activity没有onStop,我们目前没有办法将Fragment的生命周期设置到onStop。
这个Navigator定义的非常简单,不过这里面没有看到很多场景:
- 没有处理Fragment的启动方式是SingleTop的场景。
- 没有支持Fragment的入场动画。
这两个场景是否支持,可以根据自身业务场景来决定。这里就不过多介绍了,实现方式无非就是FragmentNavigator的实现代码拷贝出来。
到这里,我们只是完成自定义Navigator的第一步,还需要把自定义的Navigator挂载到NavController上面。重写NavHostFragment的onCreateNavController
方法:
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
navController.navigatorProvider.addNavigator(
KeepStateNavigator(
requireContext(),
childFragmentManager,
getContainerId()
)
)
}
然后把graph文件中定义的fragment元素,都换成
KeepStateFragment
:
然后就是把NavHostFragment换成我们自己的HostFragment:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val hostFragment = CustomNavHostFragment.create(R.navigation.default_graph)
supportFragmentManager.beginTransaction()
.add(R.id.nav_host_fragment_container, hostFragment)
// 只有设置这个属性,NavHostFragment 才能成功拦截系统的back事件。
.setPrimaryNavigationFragment(hostFragment)
.commitAllowingStateLoss()
}
}
如此,Navigator
定义便完成了。
(2). 自定义传参
在Navigation中,给页面传参有两种方式:
- 通过Bundle方式来传递,这种方式只能传递支持序列化的数据。
- 非序列化的数据可以通过Extras接口来提供,可以自行定义一个实现类,然后盛装数据,传递Navigator的navigate方法来,这里会创建Fragment的对象,同时可以数据设置到Fragment里面去。
关于这块的实现,大家可以自行实现一下,这里就不介绍了。
本小节对Navigator
定义介绍的比较简单,很多的东西其实都没有介绍。比如说,如果KeepStateNavigator
需要支持入场动画,就需要将使用FragmentManager的addToBackStack方法,那么前面提到生命周期不正确的问题该怎么解决。这类问题都是偏业务的,要针对具体的业务场景来处理,如果业务不需要有入场动画,那就不需要实现;如果业务需要支持入场动画,那么就不能出现删除返回栈中间某一个页面的场景,只能乖乖的一个一个返回退出。
4. Navigation的一些设计美学和“缺点”
本小节主要介绍一下Navigation的美学和缺点。先提前说明,下面的观点单纯的是自己的理解,不代表大众想法,大家和而不同就行了。
(1). 设计美学
- graph文件的存在,使得跳转流程可视化、配置化。
- Navigator的独立,且支持自定义化。
- 传参方法接口化,完美适配自定义需求。特别是Extras接口,让我们在此接口上可以做很多的事情,比如说自定义传参,以及可以维护自己的一套数据。
- Destination的抽象化。这个设计使得Navigation可以设置多种页面的类型,例如Activity、Fragment、Dialog等,我们还可以在此基础扩展自己的页面类型。
(2).“缺点”:
- NavOption的final,不支持自定义属性。
- NavInflater的final,不支持自定义。
- 限制太多,页面之间直接不能自由跳转,必须依赖graph文件的配置。
- 过于臃肿,引入这个库,使得Apk大小上涨了几百K。
5. 总结
尽管官方给我们提供了这个工具,包括我自己也认认真真的调研了一下这个库,但是我们业务也没有使用这个库,而是参考了这个库的实现,自己实现了一个Navigation库,主要原因有两点:
- 库过于臃肿,导致我们的包大小上涨特别多,而且我们只会使用它的Fragment切换能力,直接引入得不偿失。
- 某些要求它实现不了,比如说想要随意的删除页面栈中任何位置的页面。
基于此,我们最后决定自己来实现一个Navigation库,也欢迎大家使用我们的App--快手,其中快手App内部搜索流程就是用此方案来实现的,大家在使用过程中遇到问题或者有更好的建议都可以提给我。目前此库已经上线了一个多月了,还算稳定,暂时没有收到任何问题报告,如果可能的话,后续我考虑把此库开源,供大家参考参考。
到这里,我们来总结本篇文章的内容。
- Navigation的跳转方式有两种,分别是:action跳转和DeepLink跳转,这两种跳转方式,最终都会汇总到
Navigator
的navigate
方法里面去。popUpTo
和popUpToInclusive
本质上不是一种跳转方式,而是在在action跳转基础上,对页面栈的操作。FragmentNavigator
是通过replace方式加载一个新的Fragment,这个方式会导致旧的Fragment走到onDestroyView阶段,从而导致Fragment的onCreateView重复调用。- 页面返回方式有两个:
NavController#popBackStack
和NavController#navigateUp
。popBackStack方法表示的是退出的栈顶页面;而navigateUp
方法,当页面栈中有多个页面,跟popBackStack
方法的逻辑一致;当页面栈中只有一个页面,会finish掉当前的Activity。- 自定义Navigator的过程分为三步:首先,实现Navigator接口,定义跳转逻辑和返回逻辑;其次,将自定义的Navigator挂载到NavController上面;最后,就是将graph文件对应的元素名改为自定义的元素名。