美文网首页
Jetpack 源码分析(八) - 手把手教你认识Navigat

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

作者: 琼珶和予 | 来源:发表于2022-04-30 22:16 被阅读0次

      本文是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文件对应的元素名改为自定义的元素名。

    相关文章

      网友评论

          本文标题:Jetpack 源码分析(八) - 手把手教你认识Navigat

          本文链接:https://www.haomeiwen.com/subject/qsizqrtx.html