0
点赞
收藏
分享

微信扫一扫

JackPack原理篇(一):Navigation

IT影子 2022-01-20 阅读 57

JetPack框架出来的时间也比较久了,我记得我是在19年开始接触相关的框架,当时觉得ViewModel、LiveData、Paging2、协程之类的很厉害,很牛逼,很好用。可惜,学是学了,只是当时的环境用不上。不过有意思的是我也学了一下Navigation,当时给的评价是“鸡肋”,学之无用,弃之可惜,而且当时的Navigation总是有这样那样的问题,所以后面就放弃使用了。不过最近在管理Fragment的时候总是听别人说Navigation有多厉害多厉害的,而且正好项目也要对部分代码进行重构,所以重新再学了一下Navigation。不动笔墨不读书,因此在这里记录一下我对Navigation的理解。
Navigation的使用就没必要放上来了,这种烂大街的东西,随手可以搜得到,下面直接讲讲原理吧。

Navigation中值得关注的有两个问题,第一个是:Navigation是如何进行初始化的,即在配置好相关的nav_graph、FragmentContainerView等等之后,它是怎么工作的;第二个是:我们调用的navigate()方法,它是如何实现它的功能的。

先分析第一个问题,以下面的代码为例:

<androidx.fragment.app.FragmentContainerView
            android:id="@+id/main_fragment_container"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintTop_toTopOf="parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_graph" />

这段代码在FragmentContainerView放置了一个NavHostFragment,NavHostFragment的创建方式有两种,第一种是通过代码方式:NavHostFragment.create() ,第二种方法是通过xml的方式,就比如上面的代码。先分析第一种方式:

    public static NavHostFragment create(@NavigationRes int graphResId,
            @Nullable Bundle startDestinationArgs) {
        Bundle b = null;
        if (graphResId != 0) {
            b = new Bundle();
            b.putInt(KEY_GRAPH_ID, graphResId);
        }
        if (startDestinationArgs != null) {
            if (b == null) {
                b = new Bundle();
            }
            b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
        }

        final NavHostFragment result = new NavHostFragment();
        if (b != null) {
            result.setArguments(b);
        }
        return result;
    }

代码很简单,可以看到该方法只做了一件事,就是把导航图的id以及默认导航目的地的参数储存到一个Bundle中,并且放置到一个新创建的NavHostFragment里面。

再看看第二种方式,熟悉Fragment生命周期的都清楚,通过xml里面创建的Fragment,必然会走到onInflate方法中,其代码如下:

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

这里的代码也很简单,可以看到该方法主要做的也只有一件事:解析出xml里面的defaultNavHost属性和navGraph属性,并且将其记录起来。
根据Fragment的生命周期,其下一步会调到onCreate()方法中,其代码如下:

 public void onCreate(@Nullable Bundle savedInstanceState) {
         ......
        //①创建NavHostController对象
       
        mNavController = new NavHostController(context);
         ......
        //②创建对应的Navigator并储存到NavigatorProvider中
        onCreateNavController(mNavController);
        //③恢复数据
        Bundle navState = null;
        if (savedInstanceState != null) {
            navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
            if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
                mDefaultNavHost = true;
                getParentFragmentManager().beginTransaction()
                        .setPrimaryNavigationFragment(this)
                        .commit();
            }
            mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
        }

        if (navState != null) {
            // Navigation controller state overrides arguments
            mNavController.restoreState(navState);
        }

      //④将导航图id设置到NavController中
        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);
    }

从代码中的注释可以看到,onCreate()方法主要做了4件事:
① 初始化NavHostController
② 创建对应的Navigator并储存到NavigatorProvider中
③ 恢复相关数据
④ 将导航图设置到mNavController中

这里需要注意的是第二点和第四点。先放上onCreateNavController源码:

protected void onCreateNavController(@NonNull NavController navController) {
        navController.getNavigatorProvider().addNavigator(
                new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
        navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
    }

addNavigator源码如下:

 //getNameForNavigator(navigator.javaClass)得到的结果是对象的类的simpleName
 public fun addNavigator(
        navigator: Navigator<out NavDestination>
    ): Navigator<out NavDestination>? {
        return addNavigator(getNameForNavigator(navigator.javaClass), navigator)
    }

public open fun addNavigator(
        name: String,
        navigator: Navigator<out NavDestination>
    ): Navigator<out NavDestination>? {
        require(validateName(name)) { "navigator name cannot be an empty string" }
        val previousNavigator = _navigators[name]
        if (previousNavigator == navigator) {
            return navigator
        }
        check(previousNavigator?.isAttached != true) {
            "Navigator $navigator is replacing an already attached $previousNavigator"
        }
        check(!navigator.isAttached) {
            "Navigator $navigator is already attached to another NavController"
        }
        return _navigators.put(name, navigator)
    }

private val _navigators: MutableMap<String, Navigator<out NavDestination>> = mutableMapOf()
public inline fun <K, V> mutableMapOf(): MutableMap<K, V> = LinkedHashMap()

从上面的代码可以看出,整个onCreateNavController()方法做的只有一件事,就是创建两个Navigator:DialogFragmentNavigator和FragmentNavigator,并且将其以(key-类名,value-对象)的方式放置到NavigatorProvider中。

再看看第四点中的关键代码:mNavController.setGraph(mGraphId);

 public open fun setGraph(@NavigationRes graphResId: Int) {
        setGraph(navInflater.inflate(graphResId), null)
    }

很明显,这里先把导航图的资源ID解析,再继续往导航图里面传。可以看看导航图的解析(不想看的可以直接看结论就行):

public fun inflate(@NavigationRes graphResId: Int): NavGraph {
         ......
        return try {
             ......
            val destination = inflate(res, parser, attrs, graphResId)
            require(destination is NavGraph) {
                "Root element <$rootElement> did not inflate into a NavGraph"
            }
            destination
        }
         ......
    }

我把没用的代码删了,这里需要注意的地方有两个:
① 返回的类型是NavGraph,该类是NavDestination的子类
② inflate(res, parser, attrs, graphResId)
第二点就是具体解析导航图的地方,在这里继续放源码的作用不大,所以这个点我忽略了,有兴趣的可以自己去看下代码。我比较好奇的是NavGraph这个对象,先上源码:

/**
 * NavGraph is a collection of [NavDestination] nodes fetchable by ID.
 *
 * A NavGraph serves as a 'virtual' destination: while the NavGraph itself will not appear
 * on the back stack, navigating to the NavGraph will cause the
 * [starting destination][getStartDestination] to be added to the back stack.
 */

翻译一下:
(1)导航图是可通过ID索引NavDestination节点的集合
(2)一个导航图可以作为一个虚拟的目的地(destination,这就是我上面说的需要注意的第一点,导航图本身是继承NavDestination的):导航图本身不会出现在返回栈中,如果把导航图当做目的地去导航时,将会把导航图的的startDestination添加到返回栈中

用人话说可以理解为如果把导航图当NavDestination用,那么就会把默认的Fragment加到返回栈中。(需要注意,返回栈中并非放置Fragment,这里之所以这么说只是为了好理解)

导航图里面存放节点的是SparseArrayCompat类型,有兴趣可以看一下,这里就不仔细展开。

再回到setGraph中,其源码如下:

public open fun setGraph(graph: NavGraph, startDestinationArgs: Bundle?) {
        if (_graph != graph) {
            _graph?.let { previousGraph ->
                // Clear all saved back stacks by iterating through a copy of the saved keys,
                // thus avoiding any concurrent modification exceptions
                val savedBackStackIds = ArrayList(backStackMap.keys)
                savedBackStackIds.forEach { id ->
                    clearBackStackInternal(id)
                }
                // Pop everything from the old graph off the back stack
                popBackStackInternal(previousGraph.id, true)
            }
            _graph = graph
            onGraphCreated(startDestinationArgs)
        }
        ......
    }

因为第一次初始化,那么if (_graph != graph) 必定成立,所以我把没用的else代码删了,从代码的注释中就可以很明显的看到,setGraph就做了2件事:
① 将回退栈中的信息全部清空
② 调用onGraphCreated,该方法做的事情就是显示一个导航Fragment的视图

还是要看看onGraphCreated方法:

private fun onGraphCreated(startDestinationArgs: Bundle?) {
        ......
        if (_graph != null && backQueue.isEmpty()) {
            val deepLinked =
                !deepLinkHandled && activity != null && handleDeepLink(activity!!.intent)
            if (!deepLinked) {
                // Navigate to the first destination in the graph
                // if we haven't deep linked to a destination
                navigate(_graph!!, startDestinationArgs, null, null)
            }
        } else {
            dispatchOnDestinationChanged()
        }
    }

if (_graph != null && backQueue.isEmpty()) 这个条件在初始化时是必定成立的,在正常情况下我们使用的是Fragment作为startDestination,所以if (!deepLinked) 也成立,所以最后还是走到了navigate()方法,该方法后面会讲。这里把导航图作为NavDestination,按我们上面的说法,其实就会把startDestination对应的Fragment加载到Container中。
到这里,整个onCreate方法就讲完了,记不住那么多的可以直接去记忆onCreate中的那四个点就行。

到了这里,后面就简单很多,NavHostFragment生命周期继续往下走就是onCreateView,该方法源码如下:

    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());
        // When added via XML, this has no effect (since this FragmentContainerView is given the ID
        // automatically), but this ensures that the View exists as part of this Fragment's View
        // hierarchy in cases where the NavHostFragment is added programmatically as is required
        // for child fragment transactions
        containerView.setId(getContainerId());
        return containerView;
    }

onCreateView做的事情很简单:在通过代码去创建NavHostFragment的时候,手动new一个FragmentContainerView,并赋予其相应的ID。

生命周期继续往下走,就到onViewCreated:

    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        if (!(view instanceof ViewGroup)) {
            throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
        }
        Navigation.setViewNavController(view, mNavController);
        // When added programmatically, we need to set the NavController on the parent - i.e.,
        // the View that has the ID matching this NavHostFragment.
        if (view.getParent() != null) {
            mViewParent = (View) view.getParent();
            if (mViewParent.getId() == getId()) {
                Navigation.setViewNavController(mViewParent, mNavController);
            }
        }
    }

public fun setViewNavController(view: View, controller: NavController?) {
        view.setTag(R.id.nav_controller_view_tag, controller)
    }

onViewCreated方法做的事情也很简单,把相应的NavHostController放到view的tag中,这样在后面想要获得NavController的时候就特别简单。
到这里,我们关注的第一个点就结束了。

总结一下:
(1)在NavHostFragment的create方法或者onInflate中获取到导航图的资源id和defaultNavHost的值
(2)在onCreate中创建NavHostController,同时创建相应的Navigator,并以(key-类名,value-对象值)的方式储存到NavigatorProvider中
(3)在onCreate中将导航图设置到NavHostController,事实上就是解析相应的xml,并且返回NavGraph对象,再根据该对象调用navigate方法,将Fragment放到相应容器中。
(4)在onCreateView中兼容代码创建的NavHostFragment,给它创建一个Container并设置id
(5)最后在onViewCreated中把NavHostController储存到view的tag中,方便读取。

长吸口气,再来谈谈第二个问题:navigate()方法是怎么实现的呢?
我们还是追寻着源码来看(所有的navigate方法最后都调到下面这个navigate方法):

  public open fun navigate(
        @IdRes resId: Int,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Navigator.Extras?
    ) {
        var finalNavOptions = navOptions
        //①获取当前的节点,如果回退栈为空,则直接使用导航图;跟我们之前所说的相对应
        val currentNode = (
            if (backQueue.isEmpty())
                _graph
            else
                backQueue.last().destination
            ) ?: throw IllegalStateException("no current navigation node")

        @IdRes
        var destId = resId
        //②获取当前节点的action,并且解析出其参数(传参的参数)、navOption(动画之类)以及目的地(val node = findDestination(destId))
        val navAction = currentNode.getAction(resId)
        var combinedArgs: Bundle? = null
        if (navAction != null) {
            if (finalNavOptions == null) {
                finalNavOptions = navAction.navOptions
            }
            destId = navAction.destinationId
            val navActionArgs = navAction.defaultArguments
            if (navActionArgs != null) {
                combinedArgs = Bundle()
                combinedArgs.putAll(navActionArgs)
            }
        }
        if (args != null) {
            if (combinedArgs == null) {
                combinedArgs = Bundle()
            }
            combinedArgs.putAll(args)
        }
        if (destId == 0 && finalNavOptions != null && finalNavOptions.popUpToId != -1) {
            popBackStack(finalNavOptions.popUpToId, finalNavOptions.isPopUpToInclusive())
            return
        }
        require(destId != 0) {
            "Destination id == 0 can only be used in conjunction with a valid navOptions.popUpTo"
        }
        val node = findDestination(destId)
        if (node == null) {
            val dest = NavDestination.getDisplayName(context, destId)
            require(navAction == null) {
                "Navigation destination $dest referenced from action " +
                    "${NavDestination.getDisplayName(context, resId)} cannot be found from " +
                    "the current destination $currentNode"
            }
            throw IllegalArgumentException(
                "Navigation action/destination $dest cannot be found from the current " +
                    "destination $currentNode"
            )
        }
        navigate(node, combinedArgs, finalNavOptions, navigatorExtras)
    }

从我在上面的注释可以看到,该navigate方法做了3件事:
① 获取当前的节点
② 解析好所需传的参数、navOption以及导航的目的地
③ 继续调用navigate方法

那我们继续往下看:

private fun navigate(
        node: NavDestination,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Navigator.Extras?
    ) {
     
        val finalArgs = node.addInDefaultArgs(args)
        // Now determine what new destinations we need to add to the back stack
        if (navOptions?.shouldRestoreState() == true && backStackMap.containsKey(node.id)) {
            navigated = restoreStateInternal(node.id, finalArgs, navOptions, navigatorExtras)
        } else {
            val currentBackStackEntry = currentBackStackEntry
            //①获取navigator
            val navigator = _navigatorProvider.getNavigator<Navigator<NavDestination>>(
                node.navigatorName
            )
            if (navOptions?.shouldLaunchSingleTop() == true &&
                node.id == currentBackStackEntry?.destination?.id
            ) {
              ......
            } else {
                // Not a single top operation, so we're looking to add the node to the back stack
                //②创建返回栈元素
                val backStackEntry = NavBackStackEntry.create(
                    context, node, finalArgs, hostLifecycleState, viewModel
                )
                //③调用navigator的navigateInternal,并将返回栈元素入栈
                navigator.navigateInternal(listOf(backStackEntry), navOptions, navigatorExtras) {
                    navigated = true
                    addEntryToBackStack(node, finalArgs, it)
                }
            }
        }
       ......
    }

我还是把不重要的代码删掉,从代码中的注释可以看到,该方法事实上就做了一件事,就是获取之前创建的navigator进行导航,并且把该次导航入栈。

继续往下看:

 private fun Navigator<out NavDestination>.navigateInternal(
        entries: List<NavBackStackEntry>,
        navOptions: NavOptions?,
        navigatorExtras: Navigator.Extras?,
        handler: (backStackEntry: NavBackStackEntry) -> Unit = {}
    ) {
        addToBackStackHandler = handler
        navigate(entries, navOptions, navigatorExtras)
        addToBackStackHandler = null
    }

public open fun navigate(
        entries: List<NavBackStackEntry>,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ) {
        entries.asSequence().map { backStackEntry ->
            val destination = backStackEntry.destination as? D ?: return@map null
            val navigatedToDestination = navigate(
                destination, backStackEntry.arguments, navOptions, navigatorExtras
            )
           ......
    }

这两个方法中值得关注的地方只有一个navigate方法,继续往下看:

public open fun navigate(
        destination: D,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ): NavDestination? = destination

该方法在Navigator里面,由于我们常用的是Fragment,那么我们直接看一下它的子类FragmentNavigator里面的实现:

 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;
        }
        //①通过反射创建Fragment对象
        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);
        }
        
        //③ 将Fragment通过replace方式加载到container里面
        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;

        boolean isAdded;
        //......
        ft.commit();
        // The commit succeeded, update our view of the world
        if (isAdded) {
            mBackStack.add(destId);
            return destination;
        } else {
            return null;
        }
    }

到这里很多东西就很清楚了,该方法总结如下:
① 通过反射创建Fragment对象
② 设置该次导航动画
③ 将Fragment通过replace方式加载到container里面

需要注意的有几个点:
① 每次调用navigate方法都会创建新的Fragment,要考虑清楚每次创建Fragment可能引起的问题
② Fragment都是通过replace的方式加载到container中,所以要想清楚该种方式的生命周期是否是自己需要的。
③ setPrimaryNavigationFragment方法在每次navigate都会调用到,我们可以通过getPrimaryNavigationFragment的方式获取到栈顶Fragment(目前我使用这种方式还没有问题)。具体方式如下:

 val fragment = supportFragmentManager.findFragmentById(R.id.main_fragment_container) as NavHostFragment
 val topFragment = fragment.childFragmentManager.primaryNavigationFragment

使用java的朋友可以转成java尝试。

至于导航到Activity的代码这里就不放了,毕竟大同小异。

我对Navigation的理解就到这里了,Navigation中各个类之间的关系可以查看下图:

Navigation类图.png

举报

相关推荐

0 条评论