Navigation学习总结

Google 在官方开发工具包中(Android Jetpack)中提供了一个用于Android app导航的全新框架“Navigation”,配合IDE可以很方便的查看App中页面之间或模块之间的关联关系,这个跟IOS中StoryBoard很像。
官方文档:The Navigation Architecture Component
官方教程:Navigation Codelab
官方Demo:android-navigation

概述



使用Navigation Architecture Component(后面简称Navigation)不但可以实现App间复杂的导航关系而且还使得导航关系可视化在这一点上要比一些第三方的导航框架(ARouter等)要好的多。在Navigation框架中引入了以下几个概念需要说明下:
Destination -- 直译过来就是目的地的意思,结合Android开发环境理解,指的就是页面或者模块等。Activity、Fragment、Graph等都可以充当一个Destination。

  • Action -- 就是页面间的导航关系用于连接Destination
  • Graph -- 多个Destination通过Action连接起来就是一个Graph

Navigation 框架支持在Fragment、Activity、Graph、SubGraph、自定义Destination之间导航。包括前面提到的功能总结起来Navigation框架总共提供了以下一系列的附加功能,用于辅助开发简化开发流程:

  • 处理Fragment的事务(Transactions)
  • 为返回操作(Back & Up)提供正确的默认实现
  • 为动画和过渡提供标准的资源
  • 支持Deep link
  • 通过很少的额外操作就可以支持Navigation UI,例如Navigation Drawer、Bottom Navigation等
  • 使页面间传值变的更加安全
  • 通过IDE可以实现可视化编辑

在使用Navigation框架的时候有以下几点需要注意:

  • 使用一个栈来代表App的导航状态
  • 必须要有一个固定的起始Destination
  • 不能使用Up button退出你的程序
  • 在App任务中向上和返回按钮是等价的
  • 深度链接到目标或导航到相同的目标应产生相同的堆栈

Navigation的使用


配置IDE

要是使用Navigation框架要求你的Android Studio版本必须是3.2+,如果你的Android Studio版本是3.2,你需要进入IDE的设置界面找到“Enable Navigation Editor”选项并选中(需要重新启动Android Studio)。


image.png

配置项目



创建一个标准的Android Project然后在配置文件中(build.gradle)中配置Navigation依赖(这里需要注意Android官方更新了Support Library的命名空间具体参考官方文档),具体配置方式如下:

dependencies {
    def nav_version = "1.0.0-alpha08"

    implementation "android.arch.navigation:navigation-fragment:$nav_version" // use -ktx for Kotlin
    implementation "android.arch.navigation:navigation-ui:$nav_version" // use -ktx for Kotlin

    // optional - Test helpers
    // this library depends on the Kotlin standard library
    androidTestImplementation "android.arch.navigation:navigation-testing:$nav_version"
}

如果要使用“Safe args”特性还需要增加如下配置:

apply plugin: "androidx.navigation.safeargs"

创建Navigation Graph



关于这里官方教程有点啰嗦总结起来就是以下几步:

  1. 在你项目工程的“res”目录下创建“navigation”文件夹
  2. 在新建的“navigation”目录下右键新建一个“Navigation resource file”

完成后预览界面和源码界面分别如下图所示:


image.png

image.png

根据提示点击"+"创建几个测试用的Destination,完成后如图:

image.png

对应的源码如下:

<?xml version="1.0" encoding="utf-8"?>
<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/setting_nav_graph"
            app:startDestination="@id/mainSettingFragment">

    <fragment
            android:id="@+id/mainSettingFragment"
            android:name="com.wangqiang.pro.navigationdemo.MainSettingFragment"
            android:label="fragment_main_setting"
            tools:layout="@layout/fragment_main_setting">
        <action
                android:id="@+id/action_mainSettingFragment_to_cameraSettingFragment"
                app:destination="@id/cameraSettingFragment"/>
    </fragment>
    <fragment
            android:id="@+id/cameraSettingFragment"
            android:name="com.wangqiang.pro.navigationdemo.CameraSettingFragment"
            android:label="fragment_camera_setting"
            tools:layout="@layout/fragment_camera_setting"/>
</navigation>

然后编辑Activity的布局文件,在布局文件中增加“NavHostFragment”,其中"main_nav"就是刚刚新建的Navigation Graph文件名(main_nav.xml)代码如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"

        app:defaultNavHost="true"
        app:navGraph="@navigation/main_nav" />

</android.support.constraint.ConstraintLayout>

页面跳转



要实现从MainSettingFragment到CameraSettingFragment我们只需要在MainSettingFragment中的按钮点击事件中添加如下代码:

view.findViewById<Button>(R.id.camera)
.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_mainSettingFragment_to_cameraSettingFragment))

“action_mainSettingFragment_to_cameraSettingFragment”就是你定义的Action的id。

页面传值



官方提倡通过这种方式传递一些轻量级的数据,如果数据量比较大的情况下使用“ViewModel”在Fragment之间共享数据。被传递的数据需要在Destination上配置,配置方法有两种,可以使用IDE提供的图形界面进行配置也可以使用源码的方式直接编辑Navigation Graph源文件实现。这里我们的目标Destination(CameraSettingFragment)需要一个integer类型的“camera_id”参数,配置完成后文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<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/setting_nav_graph"
            app:startDestination="@id/mainSettingFragment">

    <fragment
            android:id="@+id/mainSettingFragment"
            android:name="com.wangqiang.pro.navigationdemo.MainSettingFragment"
            android:label="fragment_main_setting"
            tools:layout="@layout/fragment_main_setting">
        <action
                android:id="@+id/action_mainSettingFragment_to_cameraSettingFragment"
                app:destination="@id/cameraSettingFragment"/>
    </fragment>
    <fragment
            android:id="@+id/cameraSettingFragment"
            android:name="com.wangqiang.pro.navigationdemo.CameraSettingFragment"
            android:label="fragment_camera_setting"
            tools:layout="@layout/fragment_camera_setting">
        <argument
                android:name="camera_id"
                app:argType="integer"
                android:defaultValue="0" />
    </fragment>
</navigation>

完成后IDE应该是自动帮我们生成MainSettingFragmentDirections和CameraSettingFragmentArgs两个类(如果没有生成手动编译一下工程),这两个类的命名规则是分别在起始Des 提 nation和目标Destination加上“Directions”和“Args”后缀,他们的作用分别如下:

  • MainSettingFragmentDirections -- 设置要传递的参数
  • CameraSettingFragmentArgs -- 取出传递的参数

然后修改下我们的跳转代码:

//MainSettingFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    
    view.findViewById<Button>(R.id.camera).setOnClickListener {
        val action = MainSettingFragmentDirections.actionMainSettingFragmentToCameraSettingFragment().setCameraId(1)
        findNavController().navigate(action)
    }
}

取出传递参数:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    val cameraId = CameraSettingFragmentArgs.fromBundle(arguments).cameraId
}

到此为止页面间传值实现完成,如果不生效或发生错误请检查依赖组件是否正确配置。这里只是简略的说明下该框架的使用方法,作为对该框架使用流程的备忘,还有很多细节的地方没有涉及到,如果需要请自行查阅官方文档。

总结分析



用如此优雅的方式重新定义Android App中的页面导航,在这里我献上在认知范围内的所有赞美,Navigation框架的出现确实为Android App开发过程中那谜一样的跳转带来了光明与秩序。下面是我个人对Navigation Architecture Component粗鄙的认知与理解,如有不到位的地方欢迎留言(拍砖)指正。Navigation Architecture Component中主要有以下核心类组成,主要关系如图:

image.png

该图所对应的Navigation Architecture Component版本为1.0.0-alpha07由于是alpha版本所以版本之间的变化可以可能会有比较大的变化各位看官请注意。
我认为关于Navigation最核心的东西就是以上这些了,下面说下我的个人理解。先来看下NaviHostFragment的源码

public class NavHostFragment extends Fragment implements NavHost {
  private NavController mNavController;
   @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Context context = requireContext();

        mNavController = new NavController(context);
        mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());
      ....
    }
  
    @NonNull
    @Override
    public NavController getNavController() {
        if (mNavController == null) {
            throw new IllegalStateException("NavController is not available before onCreate()");
        }
        return mNavController;
    }
}

public interface NavHost {
    @NonNull
    NavController getNavController();
}

源码比较简单只有三百行左右的,这里只摘取了用于说明问题的关键代码,主要作用就是作为其它的功能页面(Fragment)的宿主(容器),实现功能页面的切换。前面Demo中在Activity中的xml布局文件中写的fragment标签就是它(NavHostFragment)。NahHostFragment里面有一个mNavController实例变量同时实现了一个NavHost的接口,这个接口只有一个getNavController方法其主要作用就是用于获取NavHostFragment的私有变量 mNavController,关于NavController的源码如下:

public class NavController {
  final Deque<NavDestination> mBackStack = new ArrayDeque<>();
  private final SimpleNavigatorProvider mNavigatorProvider = new SimpleNavigatorProvider() {
        @Nullable
        @Override
        public Navigator<? extends NavDestination> addNavigator(@NonNull String name,
                @NonNull Navigator<? extends NavDestination> navigator) {
            Navigator<? extends NavDestination> previousNavigator =
                    super.addNavigator(name, navigator);
            if (previousNavigator != navigator) {
                if (previousNavigator != null) {
                    previousNavigator.removeOnNavigatorNavigatedListener(mOnNavigatedListener);
                }
                navigator.addOnNavigatorNavigatedListener(mOnNavigatedListener);
            }
            return previousNavigator;
        }
  };
  
  public NavController(@NonNull Context context) {
        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的主要是用于控制页面(Fragment, Activity, NavGraph)的切换,主要有两个实例变量需要注意分别是mBackStack和mNavigatorProvider。加载到NavHostFragment中的页面(Destination)的栈存储结构就是通过mBackStack去记录维护。mNavigatorProvider是一个导航(跳转)策略集合,为什么要这样搞?个人觉得这里设计的就比较巧妙,这是因为同时支持Fragment,Activity和NavGraph导航(跳转)而这三种Destination的跳转方式并不一样,所以通过这种设计方法就可以支持多种跳转策略,这个策略集合默认添加了ActivityNavigator、NavGraphNavigator和FragmentNavigator。细心的你可能发现上面的源码没有FragmentNavigator,😊对上面确实没有,因为它不是在NavController实例化的时候添加的,它是是在NavHostFragment初始化的时候通过外部注册的方式添加的。理解了这一点,你就可以灵活的对Navigation框架的跳转策略进行扩展,例如你想对框架增加View之间路由(跳转)的扩展!怎么搞?你只需要写一个继承Navigator的ViewNavigator。

Navigator是什么?Action是对一个导航(或者说跳转)动作的描述,而Navigator就是Action的具体执行者,这是我能想到的对Navigator的最简洁的描述。关于Navigator的核心内容如下:

public abstract class Navigator<D extends NavDestination> {

    @Retention(RUNTIME)
    @Target({TYPE})
    @SuppressWarnings("UnknownNullness") // TODO https://issuetracker.google.com/issues/112185120
    public @interface Name {
        String value();
    }

    @Retention(SOURCE)
    @IntDef({BACK_STACK_UNCHANGED, BACK_STACK_DESTINATION_ADDED, BACK_STACK_DESTINATION_POPPED})
    @interface BackStackEffect {}
  
    private final CopyOnWriteArrayList<OnNavigatorNavigatedListener> mOnNavigatedListeners =
            new CopyOnWriteArrayList<>();
  
    @NonNull
    public abstract D createDestination();
  
      public abstract void navigate(@NonNull D destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Extras navigatorExtras);
}

关于Action的实现类叫NavAction实现很简单只有几十行代码,感兴趣可以自己看这里不做赘述。

首先看到Navigator中定义了两个注解分别是Name和BackStackEffect,作用如下:

  • Name 该注解的作用是用于自定义注册到NavigatorProvider的名称,继承Navigator的子类必须使用该注解标注。
  • BackStackEffect 该注解的作用类似android support包中的@IdRes注解,用于限定变量的取值范围(BACK_STACK_UNCHANGED, BACK_STACK_DESTINATION_ADDED, BACK_STACK_DESTINATION_POPPED),用于编译阶段的检查,在OnNavigatorNavigatedListener.onNavigatorNavigated()中有用到。

然后还定义了一个抽象的navigate(...)方法,在执行Destination间跳转的时候就是调用该方法,对应的ActivityNavigator、FragmentNavigator、GraphNavigator分别有不同的具体实现。这里我们可以看下FragmentNavigator的具体实现:

//定义注册到NavigatorProvider中的名称
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
  @Override
    public void 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;
        }
        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);
        }

        ft.replace(mContainerId, frag);
        ft.setPrimaryNavigationFragment(frag);

        final @IdRes int destId = destination.getId();
        final boolean initialNavigation = mBackStack.isEmpty();
        final boolean isClearTask = navOptions != null && navOptions.shouldClearTask();
        // TODO Build first class singleTop behavior for fragments
        final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
                && navOptions.shouldLaunchSingleTop()
                && mBackStack.peekLast() == destId;

        int backStackEffect;
        if (initialNavigation || isClearTask) {
            backStackEffect = BACK_STACK_DESTINATION_ADDED;
        } 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();
                ft.addToBackStack(Integer.toString(destId));
                mIsPendingBackStackOperation = true;
            }
            backStackEffect = BACK_STACK_UNCHANGED;
        } else {
            ft.addToBackStack(Integer.toString(destId));
            mIsPendingBackStackOperation = true;
            backStackEffect = BACK_STACK_DESTINATION_ADDED;
        }
        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();
        // The commit succeeded, update our view of the world
        if (backStackEffect == BACK_STACK_DESTINATION_ADDED) {
            mBackStack.add(destId);
        }
        dispatchOnNavigatorNavigated(destId, backStackEffect);
    }
}

巴拉巴拉很长一坨,主要的就是通过FragmentManager完成Fragment Destination的切换,剩下的就是为切换过程增加动画效果以及为代切换的Fragment设置属性以及切换数据等等。

Destination对应的实现类是NavDestination,主要有3个直接子类NavGraph、FragmentNavigator.Destination和ActivityNavigator.Destination,分别用于对导航图,Fragment和Activity的描述。多个Destination就组成了NavGraph,这里需要注意下NavGraph的子节点也可以是一个NavGraph,而且节点间可以任意调转,这说明NavGraph的数据结构是图。下面看下NavDestination的源码:

public class NavDestination {
    //跳转策略
    private final Navigator mNavigator;
    private NavGraph mParent;
    private int mId;
    private CharSequence mLabel;
    //跳转需要的参数
    private Bundle mDefaultArgs;
    private ArrayList<NavDeepLink> mDeepLinks;
    private SparseArrayCompat<NavAction> mActions;
  
    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);
        }
        mNavigator.navigate(this, finalArgs, navOptions, navigatorExtras);
    }
}

NavDestination有一个mNavigator实例变量用于存储跳转策略,因为前面说过NavDestination有多个类型(子类),不同类型的NavDestination之间的跳转策略是不一样的,NavDestination中的navigate(...)方法最终就是把跳转工作委托给了mNavigator,我通过NavController执行跳转的时候最终就是调用到了这里。mActions是当前节点可以导航(跳转)到哪些节点的一个集合,是(1: n)的关系,典型的图数据结构。

最后感谢Google赐我Navigation框架!

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

推荐阅读更多精彩内容