美文网首页Android-Jetpack
初识Jetpack -- Navigation

初识Jetpack -- Navigation

作者: MxsQ | 来源:发表于2019-01-16 12:12 被阅读0次

    前言

    Google不久前推出了Navigation框架, 此框架可以方便的管理Fragment,可以看作是针对于Fragment的路由。 看到这篇文章,可以知道Navigation是如何实现的,上手文档可以参考一下下面的博文。

    上手推荐链接
    官方文档

    注意
    如果没有 “New Resource File” 选项, 通过 File -> Default Setting -> 搜索Experimental -> 勾选 Enable Navigation Editor 。

    小插曲

    刚开始看到Navigation 时,感觉棒棒的, 因为它所带来的优点很吸引人,一者带来了方便的Fragment路由,二者支持Deep Link,此外,对于我开来说,还能带来别的好处。我在做的项目里使用了AAC框架,并针对AAC做了预加载方案,如果能结合Navigation的话,我甚至可以直接去掉已经做好的预加载方案,嘿嘿~。但是因为一些原因没有使用。来看正文吧。

    正文

    如果有尝试过使用Navigation的demo的话,会发现除了必要的XML文件以及生成的模版代码,此外没有多余的代码,那么通过Navigation类型的XML文件指定的Fragment是如何被加载的呢?

    注意到,在Activity所使用的XML文件里,有这样的一段布局代码

        <fragment
            android:id="@+id/my_nav_host"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:navGraph="@navigation/nav"
            app:defaultNavHost="true"
            />
    

    以上提供的主要信息有两个:

    1. name处指定Fragment为NavHostFragment
    2. navGraph指定引用的XML文件
      而NavHostFragment从包名上看,是系统提供的Fragment,那么可以预想到,使用了NavHostFragment作为载体。

    在Activity被加载时,其使用的XML文件里的所有节点元素也会被加载,因此,可以直接搜索定位到NavHostFragment文件, 来到onInflate(),代码如下

            final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
            // 获取必须的graphId
            final int graphId = a.getResourceId(R.styleable.NavHostFragment_navGraph, 0);
            final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
    
    // 当前NavHostFragment是作为载体并被指定的,因此能找到graphId
            if (graphId != 0) {
            // 存入graphId
                setGraph(graphId);
            }
            // 作为载体, 因为为true
            if (defaultHost) {
                mDefaultNavHost = true;
            }
            a.recycle();
    

    上面代码主要做的事情就是找到graphId,直接去看setGraph()

    // 初次加载,mNavController为空
     if (mNavController == null) {
                Bundle args = getArguments();
                if (args == null) {
                    args = new Bundle();
                }
                // 存入graphResId
                args.putInt(KEY_GRAPH_ID, graphResId);
                setArguments(args);
            } else {
                mNavController.setGraph(graphResId);
            }
    

    上面代码主要将再上一拿到的graphId存入合适的地方,接下来,就要走HostFragment的生命周期了,见onCreate()

     super.onCreate(savedInstanceState);
            final Context context = requireContext();
            // 实例化mNavController
            mNavController = new NavController(context);
            // 添加FragmentNavigator
            mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());
    
            Bundle navState = null;
            if (savedInstanceState != null) {
                navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
                if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
                    mDefaultNavHost = true;
                    // 通过FragmentManager 将HostFragment 切换到回退栈顶部
                    requireFragmentManager().beginTransaction()
                            .setPrimaryNavigationFragment(this)
                            .commit();
                }
            }
    
            if (navState != null) {
                // Navigation controller state overrides arguments
                mNavController.restoreState(navState);
            } else {
                final Bundle args = getArguments();
                final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
                if (graphId != 0) {
                    // 记录graphId , 之前存入的,并进行加载
                    mNavController.setGraph(graphId);
                } else {
                    mNavController.setMetadataGraph();
                }
            }
    

    从上面的代码来看,HostFragment也是通过FragmentManager来进行切换的,并且对NavController进行了一些操作。目前,仅仅知道HostFragment会被推到栈顶,且HostFragment是被当作载体用的,因此关于其他Fragment是如何被加载的,还未可知,因此需要继续跟进,看NavController()

      mContext = context;
            while (context instanceof ContextWrapper) {
                if (context instanceof Activity) {
                    mActivity = (Activity) context;
                    break;
                }
                context = ((ContextWrapper) context).getBaseContext();
            }
            mNavigatorProvider.addNavigator(new NavGraphNavigator(mContext));
            mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
    

    NavController 实例化的时候,添加了NavGraphNavigator,ActivityNavigator,此后在HostFragment.onCreate()又添加了FragmentNavigator, 了解一下


    Navigator.png

    源码对于Navigator的注释为:Navigator拥有自己的回退栈,并且知道如何进行导向。换句话说,Navigator是进行具体的路由导向的。

    回到NavHostFragment.onCreate(), 在将NavHostFragment推到栈顶后,将graphId拿到并交给了NavController进行加载,见mNavController.setGraph(graphId)

        public void setGraph(@NavigationRes int graphResId) {
            // 通过graphResId 进行加载
            mGraph = getNavInflater().inflate(graphResId);
            // 记住graphResId
            mGraphId = graphResId;
            // 将当前所需的页面加入栈顶
            onGraphCreated();
        }
    

    上面代码中getNavInflater(),拿到了NavInflater, 实例化代码简单,就不贴了。拿到NavInflater后,通过inflate将所需信息进行加载,见NavInflater.inflate()

        public NavGraph inflate(@NavigationRes int graphResId) {
            Resources res = mContext.getResources();
            // 拿到XML的解析器, graphResId也就是在Activity使用的XML里,app:navGraph指定的XML的id
            XmlResourceParser parser = res.getXml(graphResId);
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            try {
                ......
                // 解析出NavDestination
                NavDestination destination = inflate(res, parser, attrs);
                // 防治不合法的解析
                if (!(destination instanceof NavGraph)) {
                    throw new IllegalArgumentException("Root element <" + rootElement + ">"
                            + " did not inflate into a NavGraph");
                }
                return (NavGraph) destination;
            } 
            ......
    
    

    上面的代码主要根据XML解析出NavDestination,如果看过Navigation框架的demo或者写过的话,可以知道指定的Navigation资源文件夹下的XML,会有类似的代码

        <fragment
            android:id="@+id/oneFragment"
            android:name="com.bf.qinx.nav.fragment.OneFragment"
            android:label="fragment_one"
            tools:layout="@layout/fragment_one" >
            <action
                android:id="@+id/action_oneFragment_to_twoFragment"
                app:destination="@id/twoFragment" />
            <action
                android:id="@+id/action_oneFragment_to_threeFragment"
                app:destination="@id/threeFragment" />
        </fragment>
    

    其中的关键信息有

    1. 出发点fragment的信息
    2. action的信息,id是标示
    3. 目的地fragment的信息,见action的app节点

    现在的情况,需要插播NavDestination,源码对于NavDestination的注释是,NavDestination代表了整个 navigation graph 上的某个节点。

    在写navigation的资源文件的时候,通过Design可以看到如下图的一张图


    nav.jpg

    也就是说,Navigation提供了组成Fragment路由的地图,而NavDestination代表了每一目的地。

    注意到,在解析完NavDestination后,需要要求NavDestination为NavGraph,即NavGraph是NavDestination的子类,且通过源码注视知道NavGraph收集了各个NavDestination,也就是说,Navigation资源文件提供的地图信息,存在于NavGraph。

    回到NavInflater.inflate(),见 NavDestination destination = inflate(res, parser, attrs);

        private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
                @NonNull AttributeSet attrs) throws XmlPullParserException, IOException {
            // 为Fragment导向的话,会是FragmentNavigator
            Navigator navigator = mNavigatorProvider.getNavigator(parser.getName());
            final NavDestination dest = navigator.createDestination();
    
            dest.onInflate(mContext, attrs);
    
            final int innerDepth = parser.getDepth() + 1;
            int type;
            int depth;
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                    && ((depth = parser.getDepth()) >= innerDepth
                    || type != XmlPullParser.END_TAG)) {
                if (type != XmlPullParser.START_TAG) {
                    continue;
                }
    
                if (depth > innerDepth) {
                    continue;
                }
    
                final String name = parser.getName();
                if (TAG_ARGUMENT.equals(name)) {
                    inflateArgument(res, dest, attrs);
                } else if (TAG_DEEP_LINK.equals(name)) {
                    // 解析DeepLink 链接
                    inflateDeepLink(res, dest, attrs);
                } else if (TAG_ACTION.equals(name)) {
                    // 解析出Navigation文件所有的Destination
                    inflateAction(res, dest, attrs);
                } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
                    final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
                    final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
                    ((NavGraph) dest).addDestination(inflate(id));
                    a.recycle();
                } else if (dest instanceof NavGraph) {
                    ((NavGraph) dest).addDestination(inflate(res, parser, attrs));
                }
            }
    
            return dest;
        }
    
    

    作为初次了解的话,主要看inflateAction()即可,如下

        private void inflateAction(@NonNull Resources res, @NonNull NavDestination dest,
                @NonNull AttributeSet attrs) {
            final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavAction);
            final int id = a.getResourceId(R.styleable.NavAction_android_id, 0);
            final int destId = a.getResourceId(R.styleable.NavAction_destination, 0);
            NavAction action = new NavAction(destId);
    
            NavOptions.Builder builder = new NavOptions.Builder();
            
            //下面就是xml文件的每个Fragment节点的Action节点的子节点信息了 
            builder.setLaunchSingleTop(a.getBoolean(R.styleable.NavAction_launchSingleTop, false));
            builder.setLaunchDocument(a.getBoolean(R.styleable.NavAction_launchDocument, false));
            builder.setClearTask(a.getBoolean(R.styleable.NavAction_clearTask, false));
            builder.setPopUpTo(a.getResourceId(R.styleable.NavAction_popUpTo, 0),
                    a.getBoolean(R.styleable.NavAction_popUpToInclusive, false));
            builder.setEnterAnim(a.getResourceId(R.styleable.NavAction_enterAnim, -1));
            builder.setExitAnim(a.getResourceId(R.styleable.NavAction_exitAnim, -1));
            builder.setPopEnterAnim(a.getResourceId(R.styleable.NavAction_popEnterAnim, -1));
            builder.setPopExitAnim(a.getResourceId(R.styleable.NavAction_popExitAnim, -1));
            action.setNavOptions(builder.build());
    
            dest.putAction(id, action);
            a.recycle();
        }
    

    上面的代码就解析出了Navigation类型的XML文件的信息,上面的各种set就对应了各种属性,Navigation大致如下面的样子

    <navigation xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/nav"
        app:startDestination="@id/oneFragment">
    
        <fragment
            android:id="@+id/twoFragment"
            android:name="com.bf.qinx.nav.fragment.TwoFragment"
            android:label="fragment_two"
            tools:layout="@layout/fragment_two" ></fragment>
        <fragment
            android:id="@+id/oneFragment"
            android:name="com.bf.qinx.nav.fragment.OneFragment"
            android:label="fragment_one"
            tools:layout="@layout/fragment_one" >
            <action
                android:id="@+id/action_oneFragment_to_twoFragment"
                app:destination="@id/twoFragment" />
            <action
                android:id="@+id/action_oneFragment_to_threeFragment"
                app:destination="@id/threeFragment" />
        </fragment>
        <fragment
            android:id="@+id/threeFragment"
            android:name="com.bf.qinx.nav.fragment.ThreeFragment"
            android:label="fragment_three"
            tools:layout="@layout/fragment_three" />
    </navigation>
    

    对比之下,就一目了然了。其中的Action的id、destination等等属性,都相应被记录下来了。

    在这些步骤之后,整个Graph就解析下来了。

    回到NavController.setGraph() , 见onGraphCreated()

    private void onGraphCreated() {
    
            if (mGraph != null && mBackStack.isEmpty()) {
                boolean deepLinked = mActivity != null && onHandleDeepLink(mActivity.getIntent());
                if (!deepLinked) {
                    // 加载当前的Fragment
                    mGraph.navigate(null, null, null);
                }
            }
        }
    

    之前说过,NavHostFragment是作为载体的,因此它需要有展示内容。在解析出Graph之后,就会去加载首个需要展示的Fragment。在Navigation文件里,属性app:startDestination,就是要展示的首个Fragment。

    mGraph.navigate()将导向任务转发到了FragmentNavigator

        public void navigate(@NonNull Destination destination, @Nullable Bundle args,
                @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
            ......
            // 通过反射拿到
            final Fragment frag = destination.createFragment(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);
            }
    
            // 通过FragmentManager进行Fragment的切换
            ft.replace(mContainerId, frag);
            ft.setPrimaryNavigationFragment(frag);
    
          ......
            // 分发navigate事件
            dispatchOnNavigatorNavigated(destId, backStackEffect);
        }
    

    到这里就明白了, 切换Fragment实际上也是通过FragmentManager进行操作的,而拿到Fragment则是通过反射拿到的。

    小结

    1. NavHostFragment被用作载体,在Activity的layout文件里被指定,并指明所使用的nav
    2. 通过graphId(就是nav)拿到指定的XML文件,进行解析
    3. Navigation的XML文件被解析成Destination,并存于Graph中
    4. 通过反射拿到具体的Fragment,并切换

    下面是整个结构图


    架构图.png

    图片来源
    从结构图上看,各个角色的指责如下

    • NavHostFragment 作为载体,持有NavController
    • NavController 负责导向需求委托给Navigator,并将解析的需求教给NavGraph
    • NavInflater 负责解析Navgation文件
    • NavDestination 存有各个目的地信息

    来看一个跳转,类似如下

    Navigation.findNavController(v).navigate(R.id.action_oneFragment_to_twoFragment);
    

    首先,通过NavController转发

        public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
                @Nullable Navigator.Extras navigatorExtras) {
          ......
            // 找到NavDestination信息
            NavDestination node = findDestination(destId);
            ......
            
            // 转发        
            node.navigate(args, navOptions, navigatorExtras);
        }
    

    再由NavDestination转发

      public void navigate(@Nullable Bundle args, @Nullable NavOptions navOptions,
                @Nullable Navigator.Extras navigatorExtras) {
            Bundle defaultArgs = getDefaultArguments();
            Bundle finalArgs = new Bundle();
            finalArgs.putAll(defaultArgs);
            if (args != null) {
                finalArgs.putAll(args);
            }
            // 这里是FragmentNavigator
            mNavigator.navigate(this, finalArgs, navOptions, navigatorExtras);
        }
    }
    

    最后由Navigator进行导向,之后的过程,就和之前的类似了,不多说。

    其它
    回退操作navigateUp(),是通过NavController操作回退栈进行了,有兴趣再观看

    相关文章

      网友评论

        本文标题:初识Jetpack -- Navigation

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