Jetpack 源码分析(八) - 手把手教你认识Navigation(下)

  本文是Navigation分析文章的下篇,内容续Jetpack 源码分析(七) - 手把手教你认识Navigation(上)。在阅读本文之前,推荐优先看上篇的内容,方便很多的知识点能够串联起来。本文的主要内容是:

  1. 跳转逻辑的实现。
  2. FragmentNavigator的分析。
  3. 如何自定义Navigator,如何自定义传参?
  4. Navigation的一些设计美学和"缺点"。

  本文参考文章:

  1. Navigation的官方文档
  2. 【背上Jetpack之Navigation】想去哪就去哪,Android世界的指南针
  3. Navigation的源码解析
  4. Jetpack 源码分析(七) - 手把手教你认识Navigation(上)

  本文demo代码:NavigationDemo

1. 跳转逻辑的实现

  在上篇内容中,我们已经知道页面可以使用NavController的navigate方法来实现跳转,同时在前面也简单介绍过跳转的流程。但是,前面介绍的知识过于简单和笼统,其内部的实现原理并没有过多的介绍。因此,在这里,我们深入源码中去介绍其的实现原理。
  本节主要内容是:

  1. 初始化的跳转,即graph的startDestination。
  2. action的跳转。
  3. deeplink跳转。
  4. popUpTo 和 popUpToInclusive的特别分析。

(1). 初始化跳转

  我们在上篇内容中已经说过,当NavController在inflate graph的时候,其实还做了一件事,那就是需要跳转到该graph使用startDestination标记的页面。那么这个初始化跳转做了哪些事呢?我们应该需要注意哪些问题呢?我们直接来看代码,首先看NavControlleronGraphCreated方法:

    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();
        }
    }

  这个方法里面的代码主要分为两个部分:

  1. 如果当前的返回栈是空的,那么就调用navigate方法跳转到默认页面上去。
  2. 如果返回栈不为空,那么调用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;
    }

  针对于这段代码,主要做了如下几件事:

  1. 从Intent获取对应的DeepLink,如果Intent没有直接携带DeepLink的信息,那就根据从Intent的Data信息,尝试解析出对应的DeepLink。
  2. 验证DeepLink的合法性。首先是看解析出来的DeepLink是否为空,如果不为空的话,需要看一下每个Destination的id name是否合法。id name 是否合法,主要是根据id对应的int值去Resource去寻找对应的name,如果能够找到就表示合法的。
  3. 如果Intent的Flag表示需要新启Task,且不用清空任务栈,直接新启Activity。
  4. 如果Intent的Flag表示需要新启Task,且需要清空任务栈,不用新启Activity,直接clear掉当前返回栈中不合理的页面,然后导航到DeepLink的页面。
  5. 如果不用新启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方法实现导航逻辑,主要分为如下三步:

  1. 根据NavDestination的NavigatorName获取对应的Navigator对象,而NavigatorName就是每个Navigator类前面的Navigator.Name注解,比如如下:
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {

这里我们的NavDestination是NavGraph,拿到的自然是NavGraphNavigator对象。

  1. 拿到Navigator对象之后,就是调用其navigate方法,实现页面跳转的逻辑。此方法会返回的是一个新的NavDestination对象,如果返回值不为空,表示跳转到新的页面;如果返回值为空,表示没有跳转到新的页面。
  2. 在第二步中拿到返回值之后,如果返回值不为空,那么就会将新的页面包装成为NavBackStackEntry对象放到返回栈中。这块逻辑听上去比较简单,实际上还做了很多的其他的事情,比如说目标页面又是一个NavGraph等。不过这里我们只考虑正常的跳转,也就是说就只有一个页面进入返回栈,这样理解起来比较简单。如果返回值为空,那么就要看看页面启动方式是否是SingleTop,如果是,就需要更新栈顶NavBackStackEntry的信息。

  接下来,我们来看看NavGraphNavigatornavigate方法是怎么实现初始化跳转的。

    @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内部方式基本类似。其中,需要注意的是:

  1. 这里的StartDestination就是我们在graph文件里面定义的startDestination属性。
  2. 这里拿到的Navigator可能是NavGraphNavigatorFragmentNavigator,甚至是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);
    }

  这个方法的代码有点长,但是实际上所做的事情很简单:

  1. 根据传递的action id,在当前NavDestination中寻找对应的NavAction。从这里我们可以知道,为啥局部action只能当前页面自己使用了,如果这里使用的是其他页面的局部action,这里就会找不到对应的NavAction。
  2. 找到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寻找对应页面的过程,就是一个正则表达式匹配的过程,我们来看一下NavGraphmatchDeepLink方法:

    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;
    }

  这个方法里面主要做了两件事:

  1. 调用super的matchDeepLink方法,进行正则匹配,去寻找合适的页面。
  2. 无论第一步是否找到对应页面,都要去NavGraph的child里面去寻找合适的页面,且最终的返回的结果是匹配度最高的页面。

  找到对应的页面之后,就正常调用对应的navigate方法进行跳转。这里就不重复介绍了。

(3). popUpTo 和 popUpToInclusive

  需要提前说明的时候,popUpTo本质上不是一种跳转方式,它依赖于action跳转。它所表示的含义是,在action跳转过程中所做一些事情。
  popUpTo主要分为两种情况:

  1. 配置了popUpTo属性,且在跳转过程中,返回栈已有对应页面的实例。此时会清空此页面之上的所有实例,而是否清空自己由popUpToInclusive控制。然后创建此页面的新实例放入返回栈。
  2. 配置了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的作用不仅仅是用来实现页面跳转的,它的作用可以定义为维护页面,包括页面跳转和页面返回。
  本小节主要内容是:

  1. 页面跳转逻辑的实现
  2. 页面返回逻辑的实现。特别区分popBackStacknavigateUp

(1). 页面跳转逻辑的实现

  我们直接看FragmentNavigatornavigate方法:

    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方法主要是做了如下几件事:

  1. 首先是创建Fragment的对象。这里不得不说几句,Navigation默认创建Fragment创建对象,我们想要在给Fragment传递一些参数都非常困难,同时创建方式还是默认的,不能自定义。
  2. 使用replace方式展示页面。使用replace的方式需要的注意的是,上一个页面会onDestroyView,这也是前面说的onCreateView可能会重复调用的原因所在。
  3. 将对应页面添加到返回栈中。这一步主要是Fragment的入场动画生效,要想一个Fragment入场动画生效,必须将其Fragment添加到FragmentManager的返回栈,也是调用FragmentManager的addToBackStack方法。需要注意的是,慎用addToBackStack方法,因为在我实际开发过程中发现,如果一个Fragment被添加到FragmentManager的返回栈中,此时直接remove此Fragment,Fragment的生命周期只会走到onDestroyView,不会到onDestroy,这是因为FragmentManager在处理生命周期的时候,会判断此Fragment是否在返回栈中。所以,如果我们页面栈中有很多的Fragment,这些Fragment都在FragmentManager的返回栈中,如果想要移除页面栈中某个页面,又不想remove此页面之上的页面,此时Fragment生命周期就会出问题。进而,如果使用了addToBackStack方法,一定要使用popBackStack方法弹出此Fragment。
  4. 最后就是返回新页面的实例。

  从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不支持随意删除返回栈的元素,有点鸡肋。算了算了,大家还是慎用addToBackStackpopBackStack方法,最好是自定义Navigator。

  在NavController中,有两个方法都可以完成返回逻辑,那就是popBackStacknavigateUp。这里就不对两个方法进行展开,我就简单说一下这两个的区别:

popBackStack:如果返回栈不为空,会一个一个退出页面栈的页面;如果返回栈为空,不做任何处理。
navigateUp: 如果当前返回栈中只有一个页面,会把当前Activity给finish;如果页面栈中有多个页面,会调用popBackStack方法执行返回逻辑。

  这就是这两个方法最大的差别,大家可以根据自身的场景,来决定调用哪个方法。详细的区别可以参考对应的源码实现和Difference between navigateUp() and popBackStack()

3. 自定义Navigator

  既然Google爸爸提供的FragmentNavigator不合我们的心意,那么我们可以自定义自己的Navigator,那么怎么自定义Navigator呢?本小节来重点介绍,主要内容如下:

  1. 自定义Navigator,正确维护Fragment的生命周期。
  2. 自定义传参。

(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
        )
    }
}

  如上代码注意几点:

  1. KeepStateNavigator前面需要加上Navigator.Name注解,注解设置的名称就是该Navigator能够处理的Destination,这个会在graph文件使用得到。
  2. 重写createDestinationnavigatepopBackStack方法。其中navigate方法就是一个新的页面入栈,popBackStack方法就是栈顶页面出栈。

  从KeepStateNavigator实现上来看,我们使用的是add方式添加Fragment,跟原生的replace方式是不一样;其次,为了保证Fragment生命周期的正确性,我们在新页面入栈的时候,会通过setMaxLifecycle方法把旧页面的生命周期设置到STARTED,在栈顶页面出栈的时候,会把新的栈顶页面生命周期设置到RESUMED。这个操作只能让Fragment在onPause和onResume之间转化,这也是我们能做到的最大限度了,一般来说已经能够覆盖很多业务场景。PS:如果Activity没有onStop,我们目前没有办法将Fragment的生命周期设置到onStop。
  这个Navigator定义的非常简单,不过这里面没有看到很多场景:

  1. 没有处理Fragment的启动方式是SingleTop的场景。
  2. 没有支持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中,给页面传参有两种方式:

  1. 通过Bundle方式来传递,这种方式只能传递支持序列化的数据。
  2. 非序列化的数据可以通过Extras接口来提供,可以自行定义一个实现类,然后盛装数据,传递Navigator的navigate方法来,这里会创建Fragment的对象,同时可以数据设置到Fragment里面去。

  关于这块的实现,大家可以自行实现一下,这里就不介绍了。

  本小节对Navigator定义介绍的比较简单,很多的东西其实都没有介绍。比如说,如果KeepStateNavigator需要支持入场动画,就需要将使用FragmentManager的addToBackStack方法,那么前面提到生命周期不正确的问题该怎么解决。这类问题都是偏业务的,要针对具体的业务场景来处理,如果业务不需要有入场动画,那就不需要实现;如果业务需要支持入场动画,那么就不能出现删除返回栈中间某一个页面的场景,只能乖乖的一个一个返回退出。

4. Navigation的一些设计美学和“缺点”

  本小节主要介绍一下Navigation的美学和缺点。先提前说明,下面的观点单纯的是自己的理解,不代表大众想法,大家和而不同就行了。

(1). 设计美学

  1. graph文件的存在,使得跳转流程可视化、配置化。
  2. Navigator的独立,且支持自定义化。
  3. 传参方法接口化,完美适配自定义需求。特别是Extras接口,让我们在此接口上可以做很多的事情,比如说自定义传参,以及可以维护自己的一套数据。
  4. Destination的抽象化。这个设计使得Navigation可以设置多种页面的类型,例如Activity、Fragment、Dialog等,我们还可以在此基础扩展自己的页面类型。

(2).“缺点”:

  1. NavOption的final,不支持自定义属性。
  2. NavInflater的final,不支持自定义。
  3. 限制太多,页面之间直接不能自由跳转,必须依赖graph文件的配置。
  4. 过于臃肿,引入这个库,使得Apk大小上涨了几百K。

5. 总结

  尽管官方给我们提供了这个工具,包括我自己也认认真真的调研了一下这个库,但是我们业务也没有使用这个库,而是参考了这个库的实现,自己实现了一个Navigation库,主要原因有两点:

  1. 库过于臃肿,导致我们的包大小上涨特别多,而且我们只会使用它的Fragment切换能力,直接引入得不偿失。
  2. 某些要求它实现不了,比如说想要随意的删除页面栈中任何位置的页面。

  基于此,我们最后决定自己来实现一个Navigation库,也欢迎大家使用我们的App--快手,其中快手App内部搜索流程就是用此方案来实现的,大家在使用过程中遇到问题或者有更好的建议都可以提给我。目前此库已经上线了一个多月了,还算稳定,暂时没有收到任何问题报告,如果可能的话,后续我考虑把此库开源,供大家参考参考。
  到这里,我们来总结本篇文章的内容。

  1. Navigation的跳转方式有两种,分别是:action跳转和DeepLink跳转,这两种跳转方式,最终都会汇总到Navigatornavigate方法里面去。
  2. popUpTopopUpToInclusive本质上不是一种跳转方式,而是在在action跳转基础上,对页面栈的操作。
  3. FragmentNavigator是通过replace方式加载一个新的Fragment,这个方式会导致旧的Fragment走到onDestroyView阶段,从而导致Fragment的onCreateView重复调用。
  4. 页面返回方式有两个:NavController#popBackStackNavController#navigateUp。popBackStack方法表示的是退出的栈顶页面;而navigateUp方法,当页面栈中有多个页面,跟popBackStack方法的逻辑一致;当页面栈中只有一个页面,会finish掉当前的Activity。
  5. 自定义Navigator的过程分为三步:首先,实现Navigator接口,定义跳转逻辑和返回逻辑;其次,将自定义的Navigator挂载到NavController上面;最后,就是将graph文件对应的元素名改为自定义的元素名。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容