初识Jetpack -- Navigation

前言

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操作回退栈进行了,有兴趣再观看

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

推荐阅读更多精彩内容