一、前言

    传统的应用开发,一般都是采用一个界面一个 Activity 的形式,但是大家都知道, Activity 在 Android 中是属于重量级的组件,从而导致程序资源消耗大,用户体验不佳。而导航组件 Navigation 采用的是 Fragment 轻量级的组件实现的,可以节省资源,提高用户体验。

二、导航简介

    导航组件是 Android Jetpack 的一部分,主要用途是实现用户导航、进入和退出应用中不同内容片段的交互,不论是普通的按钮点击,还是应用栏、抽屉导航栏等复杂的模式,它都能轻松应对,当然,导航组件也有它既定的 导航原则 来确保一致且可预测的用户体验。

2.1 导航组件的组成

导航组件主要有三部分组成:

  • 导航图(navGraph):这是包含所有导航相关信息的 XML 资源,这些信息包括应用内所有内容区域个体(称为目标,一般都是 Fragment),以及用户可以通过应用跳转的可能路径。
  • 导航宿主(NavHost):这是用来显示导航图中声明的目标的一个空白容器。导航组件包含一个默认的 NavHost 实现 (NavHostFragment),可用于显示 Fragment 目标。
  • 导航控制器(NavController):在 NavHost 中管理应用导航的目标,当用户在应用中进行操作时,导航控制器会控制目标的切换。

使用导航组件有各种优势,包括以下方面:

  • 自动处理 Fragment 事务;
  • 在默认情况下,能够正确处理往返操作;
  • 支持动画和转场动画;
  • 支持导航界面模式(例如:抽屉式导航栏和底部导航栏)
  • Safe Args 支持(一种可在导航和目标之间传递数据时提供类型安全的 Gradle 插件)
  • ViewModel 支持
  • 可以使用Android Studio的 Navigation Editor 来编辑和查看导航图(必须使用AnroidStudio 3.3及以上版本)

2.2 导航的原则

    在使用导航组件时,应当遵循一些原则,以提高用户体验。

注意:即使您未在项目中使用 Navigation 组件,您的应用也应遵循这些设计原则。

2.2.1 固定的起始目的地

    顾名思义,您构建的应用必须有一个固定的起始目的地,这个起始目的地就是指当应用启动时蛋刀的第一个屏幕。起始目的地也是用户按返回按钮后,在回到启动器前看到的最后一个屏幕。

示例:
起始目的地
    以上示例中,用户登录页面就是起始目的地,点击启动器图标打开应用,第一个启动的页面就是用户登录页面,在返回过程中,最后一个呈现的页面也是登录页面。

2.2.2 导航状态表现为目的地堆栈

    在用户启动应用时,系统会启动一个新任务,并且显示起始目的地,这个起始目的地是应用导航的而基础。当用户在应用中进行导航时,栈顶的目标就是显示在屏幕上的,而栈内的所有目标都是历史记录。

导航组件会为你管理所有返回栈的顺序,当然你也可以自行管理,已达到某些目的。

2.2.3 在应用的任务中向上按钮和返回按钮行为相同

    首先,说一下什么是向上按钮,向上按钮是指在应用中的返回上一级的按钮(一般是在用户导航栏中),返回按钮则是系统导航中的返回按钮。在应用的任务重,向上按钮和返回按钮的行为相同,都是将栈顶的目标移除,返回到上一个目标。

2.2.4 向上按钮不会退出应用

    在应用的任务重,向上按钮可以返回到上一个目标,但是绝不会退出应用。

2.2.5 深度链接可以模拟手动导航

    无论是通过深度链接至特定的目的地,还是手动导航到特定目的地,都可以使用向上按钮通过各个目的地导航回到起始目的地。当深度链接至特定的目的地时,会移除所有返回栈中的任务,并替换为深度链接的返回栈。值得注意的是,深度链接合成的返回栈是一个完整的返回栈,他跟手动导航至特定目的地具有相同的返回栈,这个是非常重要的,因为合成的返回栈必须是真实的。

三、使用入门


本文对应的Demo源码请访问GithubNavigationDemo


3.1 添加依赖

    在应用模块目录下的 build.gradle文件中添加 dependencies 依赖声明。

dependencies {
      def nav_version = "2.2.2"

      // Java language implementation
      implementation "androidx.navigation:navigation-fragment:$nav_version"
      implementation "androidx.navigation:navigation-ui:$nav_version"

      // Kotlin
      implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
      implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

      // Dynamic Feature Module Support
      implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"

      // Testing Navigation
      androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
    }

3.2 创建导航图资源文件

    导航是发生在各个目的地之间的,而这些目的地通过操作连接在一起。导航图是一种资源文件,它包含了所有的目的地和操作的声明。

    创建导航图资源文件,可以按以下不走进行:

  1. 在项目程序模块下面的 res 目录下,右键-》“New”-》“Android Resource File”;
  2. 在弹出的窗口中输入文件名;
  3. 在“Resource type”中选择“Navigation”;
  4. 点击确定创建资源文件。

创建导航图资源文件
    新建的导航图资源文件是一个 XML 资源文件,以 navigation 为根节点,大致内容如下。

<?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/nav_graph">
</navigation>

     <navigation> 元素是导航图的根元素。当您向图表添加目的地和连接操作时,可以看到相应的 <destination><action> 子元素。如果有嵌套图表,也会出现 <navigation> 子元素。

如果是首次添加导航图资源,Android Studio 会在 res 目录内创建一个 navigation 资源目录,该目录包含您的导航图资源文件。当然,如果你已经够熟练,可以直接创建目录和文件的方式创建。

3.2.1 Android Studio中的Navigation Editor

    Android Studio提供了强大的导航编辑器,在这里不但可以预览您所添加的目标,还可以修改导航图,可以通过拖动的方式或者直接编码修改底层 XML 的方式修改导航图。为了方便项目的维护和代码可读性,笔者更加建议使用修改底层 XML 的方式,或者结合修改底层 XML 的方式。

温馨提示:不同版本的Android Studio的界面操作有些不一样,不少从旧版升级到3.6之后,发现打开资源文件的时候,默认是 “Design” 模式(包括layout布局资源),一时间找不到北了,不知道如何切换成修改底层 XML 的模式。其实这是3.6 版本之后的小变动,在旧版本只有 “Code” 和 “Design” 两种模式,新版有 “Code”、“Split” 、“Design”三种模式,而且模式切换的位置也变了,旧版是在左下角,而新版是在右上角,而且是图标的形式(如下图)。
Android Studio中的Navigation Editor

3.3 向 Activity 添加导航宿主(NavHost)

    导航宿主是导航组件的核心部分之一,导航宿主是一个空容器,用来存放和处理目的地。导航宿主必须派生于 NavHostNavHostFragment 是导航组件的默认导航宿主实现,负责处理 Fragment 目的地的交换。

注意:导航组件的设计理念是用于具有一个主 Activity 和多个 Fragment 目的地的应用,主 Activity 与导航图相关联,并且包含一个负责根据需要交换目的地的 NavHostFragment。如果您的应用需要在多个 Activity 上实现导航,就需要为每个 Activity 添加导航宿主,并在每个 Activity 关联其自己的导航图。

3.3.1 通过 XML 添加 NavHostFragment

    在主 Activity 的布局文件中,添加 <fragment> 标签,并在内部指定导航图,如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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/fragment_main"
        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/nav_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>

详细解说:

  • android:name 属性包含 NavHost 实现类的名称(示例中使用的是默认实现 NavHostFragment,如果有需要,可以使用自定义的Fragment类,但是必须实现 NavHost 或者继承 NavHostFragment
  • app:navGraph 属性将导航宿主(NavHostFragment)与导航图关联,指向包含所有导航目的地的导航图资源文件
  • app:defaultNavHost="true" 属性确保导航宿主会拦截系统返回按钮。请注意,只能有一个默认导航宿主,如果同一布局(例如,双窗格布局)中有多个导航宿主,请务必仅指定一个默认导航宿主。

说明:导航组件是 Android Jetpack 部分,不属于 Android 系统组件,所以需要在布局中添加属性引入,如:xmlns:app="http://schemas.android.com/apk/res-auto"

    除此之外,还可以使用 Layout Editor 向 Activity 添加导航宿主,具体步骤如下:

  1. 打开 Activity 布局文件,切换到 “Design” 窗口;
  2. 在 “Palette” 窗口选择 “Containers” ,然后找到 “NavHostFragment”(可直接搜索);
  3. 将 “NavHostFragment” 拖向布局中;
  4. 在弹出的窗口中选择导航图,然后确定;
  5. 在 “Properties” 窗口设置相关属性。

3.3.2 向导航图中添加目的地

    对于懒于编码的小伙伴可以使用 Navigation Editor 向导航中添加目的地,因为这些都是用户引导模式的,没什么可说,我这里主要讲一下手动添加目的地的步骤:

  1. 新建 Fragment 类和布局文件,并实现相关逻辑代码;
  2. 在导航图 XML 中新增 <fragment> 标签;
  3. 配置 <fragment> 标签的相关属性,如:android:idandroid:nameandroid:lable 等;

示例:

<?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/nav_graph"
    app:startDestination="@id/loginFragment">

    <fragment
        android:id="@+id/loginFragment"
        android:name="com.owen.navigationdemo.LoginFragment"
        android:label="LoginFragment"
        tools:layout="@layout/fragment_login">
        <action
            android:id="@+id/action_loginFragment_to_registerFragment"
            app:destination="@id/registerFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"/>
    </fragment>
    <fragment
        android:id="@+id/registerFragment"
        android:name="com.owen.navigationdemo.RegisterFragment"
        android:label="RegisterFragment"
        tools:layout="@layout/fragment_register">
    </fragment>
</navigation>

目的地属性详解

  • Type:即标签名称,指示在源代码中,该目的地是作为 FragmentActivity还是其他自定义类实现的
  • anroid:label:这个属性指定目的地的名称
  • android:id: 这个属性指定改目的地的ID,用于在代码中引用该目的地
  • android:name:这个属性用来指定目的地所关联的类
        除此之外,还可以通过tools:layout属性指定预览布局文件,这样就可以在导航图编辑器中看到对应的布局预览。

3.3.3 将某个目的地指定为起始目的地

    导航的原则之一就是固定的起始目的地,指定起始目的地的方法有两种,一种是使用 Navigation Editor,在 “Design”窗口中,选中需要指定为起始目的地的目标,点击 “房子”图标(如下图)即可。另一种方法就是在 XML 源代码中,在 <navigation> 标签中添加 app:startDestination 属性进行指定,属性值为需要指定的目的地的ID(如下示例)。
使用Navigation Editor设置起始目的地

示例:通过 XML 代码指定起始目的地

<?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/nav_graph"
    app:startDestination="@id/loginFragment">
    <fragment
        android:id="@+id/loginFragment"
        android:name="com.owen.navigationdemo.LoginFragment"
        android:label="LoginFragment"
        tools:layout="@layout/fragment_login">
        <action
            android:id="@+id/action_loginFragment_to_registerFragment"
            app:destination="@+id/registerFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"/>
    </fragment>
    <fragment
        android:id="@+id/registerFragment"
        android:name="com.owen.navigationdemo.RegisterFragment"
        android:label="RegisterFragment"
        tools:layout="@layout/fragment_register">
    </fragment>
</navigation>

3.3.4 连接目的地

    目的地之间的逻辑连接也叫做操作,操作一般是将一个目的地连接到另一个目的地,当然,你也可以定义 全局操作 ,这类操作可以在任意位置跳转到指定的目的地,这个我们在后面会详细讲到。
     您可以使用 Navigation Editor 连接两个目的地,直接拖动箭头即可,在这里就不多介绍这种方式,直接介绍通过修改 XML 源码的方式(其实使用 Navigation Editor 也会自动修改 XML 源码),具体步骤如下:

  1. <fragment> 标签内部新增 <action> 标签;
  2. 配置anroid:idapp:destnation 属性;
  3. 如果需要,可配置app:enterAnimapp:exitAnimapp:popEnterAnimapp:popExitAnim属性定义动画。

详细解说:

  • Type:即 <action> 标签;
  • anroid:id:这个字段是操作ID,代码中通过这个ID执行操作;
  • app:destnation:这个字段是操作的目的地,用来指定操作跳转的目的地。
<?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/nav_graph"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.owen.navigationdemo.HomeFragment"
        android:label="HomeFragment"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_loginFragment"
            app:destination="@+id/loginFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
    </fragment>

    <!-- ...... -->
</navigation>

3.3.5 导航到目的地

     完成了导航图的各种配置,那么就需要在代码中实现导航到目的地了。导航到目的地是使用 NavController 完成的,这是在导航宿主中管理导航的对象的,每个导航宿主都有自己的相应导航控制器(NavController)。导航到目的地的步骤如下:

  1. 检索导航控制器;
  2. 导航到目的地。
3.3.5.1 检索导航控制器

     检索导航宿主的导航控制器,可以通过以下方法:

Kotlin:

Java:

说明:Kotlin可以直接在FragmentView以及Activity使用findNavController是因为使用了扩展方法,当然,也可以直接跟Java那样调用对应的接口。

class HomeFragment: Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_home, container, false)

        view.findViewById<TextView>(R.id.tvNickname).setOnClickListener {
            // Kotlin 扩展方法检索当前导航宿主的导航控制器
            val navController = findNavController()
        }

        return view
    }
}
3.3.5.2 导航到目的地

     检索到导航控制器之后,使用导航控制器类的 NavController.navigate()API 导航到指定的目的地,NavController.navigate()有多个变体,这里就以使用目的地ID进行导航为例:

class HomeFragment: Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_home, container, false)

        view.findViewById<TextView>(R.id.tvNickname).setOnClickListener {
            // Kotlin 扩展方法检索当前导航宿主的导航控制器
            val navController = findNavController()
            navController.navigate(R.id.action_homeFragment_to_loginFragment)
        }

        return view
    }
}
3.3.5.3 返回到指定目的地

    返回到指定的目的地,是指返回到之前导航过的目的地,这些目的地必须是在任务栈内的,可以通过 NavController.popBackStack() 接口返回上一级,或者通过 NavController.popBackStack (int destinationId, boolean inclusive) 返回到指定的某个目的地。

    到这里,已经基本掌握了导航组件的使用,后续的章节会进行更加深入地介绍导航组件的使用。

四、进阶之路

4.1 创建不同类型的导航目的地

    在导航图中,导航目的地不局限于 Fragment,其实还可以是 ActivityDialogFragment 甚至是嵌套的导航图navigation(即<navigation>内部再嵌套一个<navigation>)。详细的添加导航目的地的方法已经在 3.3.2 向导航图中添加目的地 详细说明了,这里就不在累赘。

<?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/nav_graph"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.owen.navigationdemo.HomeFragment"
        android:label="HomeFragment"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_loginFragment"
            app:destination="@+id/loginFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />

        <action
            android:id="@+id/action_homeFragment_to_settingsActivity"
            app:destination="@+id/settingsActivity"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />

    </fragment>

    <!--  ...... -->

    <dialog
        android:id="@+id/tips"
        ....... />

    <activity
        android:id="@+id/settingsActivity"
        android:name="com.owen.navigationdemo.SettingsActivity"
        android:label="SettingsActivity"
        tools:layout="@layout/activity_settings" />

</navigation>

注意:
1. 如果目的地是 Activity 类型,转场动画必须结合 Activity 处理,仅仅在导航连接目的地中声明动画,弹出动画将达不到预期的效果(参考:将弹出动画应用于 Activity 目的地过度);
2. 如果目的地是 Activity 类型,实际上就是已经离开了当前的导航组件范围;
3. 如果使用 <dialog> 声明导航目的地,必须是DialogFragment类型(包括其子类)。

4.2 嵌套导航图

    所谓的嵌套导航图,就是在导航图内再嵌入一个导航图,外部的称为父导航图,内部的叫子导航图。嵌套的导航图封装着自己的目的地,且必须标识起始目的地,父导航图访问子导航图只能通过子导航图的起始目的地(不能直接访问子导航图中的目的地),因为子导航图拥有不一样的导航控制器(NavController)。使用嵌套导航图可以对导航目的地进行分类封装,防止错误的访问。

    嵌套导航图有两种表现形式,一种是在导航图 <navigation>标签内部嵌套一个 <navigation> 标签;另一种是使用 include 标签引入导航图资源文件。

注意事项:
1. 两种表现形式效果是一样的,如果导航图比较复杂,使用第二种会使得导航图资源显得更加简;
2. 父导航图中访问子导航图,不能直接访问子导航图中的目的地,只能通过子导航图ID访问子导航图的起始目的地.

4.2.1 在导航图 <navigation>标签内部嵌套一个 <navigation> 标签

示例:

<?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/nav_graph_login"
    app:startDestination="@id/homeFragment">

    <!-- ...... -->

	<!-- 内嵌导航图(子导航图),必须指定起始目的地 -->
    <navigation android:id="@+id/Settings"
        app:startDestination="@id/settingsFragment">

        <!-- ...... -->
    </navigation>
</navigation>

4.2.2 使用 include 标签引入导航图资源文件

示例:

  • 定义一个导航图资源文件(nav_graph_settings.xml
<?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/Settings"
    app:startDestination="@+id/settingsFragment">

    <!-- ....... -->
</navigation>
  • 在主导航图中使用 include 引入导航图资源
<?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/nav_graph_login"
    app:startDestination="@id/homeFragment">

  
    <!-- ........ -->

	<!-- 注意:引入导航图资源使用@navigation -->
    <include app:graph="@navigation/nav_graph_settings" />
</navigation>

4.3 全局操作

    操作就是目的地之间的跳转(<action>),全局操作就是指在导航图内所有目的地都能执行的操作。通常我们在目的地标签内部声明操作(<action>),但是全局操作是在导航图内声明(<navigation> 标签下)。全局操作的使用跟普通的操作一样,不同的是他可以在当前导航图下所有的目的地内都可以使用。

注意事项:
1. 全局操作只能在同一导航图内的目的地中调用,不可在所声明的导航图外部使用,即使是子导航图中的目的地也不允许;
2. 全局操作的目的地必须是当前导航图下的目的地或者子导航图入口(子导航图中的目的地也是不允许的)

示例:

<?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/Settings"
    app:startDestination="@+id/settingsFragment">


    <!-- 全局操作,在navigation标签下声明,目的地必须是当前导航图下的目的或者子导航图入口 -->
    <action
        android:id="@+id/action_to_settings_more"
        app:destination="@+id/commonFragment"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right" />

    <!-- ........ -->

    <fragment
        android:id="@+id/commonFragment"
        android:label="CommonFragment"
        android:name="com.owen.navigationdemo.CommonFragment"
        tools:layout="@layout/fragment_common" />
</navigation>

4.4 在目的地之间传递数据

    导航支持您通过定义目的地参数将数据附加到导航操作,在不同目的地之间实现数据传递。

提示:建议仅在目的地之间传递最少量的数据,因为在 Android 上用于保存所有状态的总空间是有限的,如果需要传输大量数据,可以考虑其他替代方案。

4.4.1 定义参数

    在导航图的操作中可以定义参数,在操作中定义的参数,有几个属性:

  • android:name 参数名
  • android:defaultValue 参数默认值
  • app:argType 参数类型
  • app:nullable 是否可为空

示例:

<?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/nav_graph_login"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.owen.navigationdemo.HomeFragment"
        android:label="HomeFragment"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_loginFragment"
            app:destination="@+id/loginFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" >
        </action>
        <!-- ...... -->
    </fragment>
    
    <fragment
        android:id="@+id/loginFragment"
        android:name="com.owen.navigationdemo.LoginFragment"
        android:label="LoginFragment"
        tools:layout="@layout/fragment_login">
        <action
            android:id="@+id/action_loginFragment_to_registerFragment"
            app:destination="@+id/registerFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"/>
        <argument
            android:name="type"
            app:argType="integer"
            android:defaultValue="0"
            app:nullable="false"/>
        <argument
            android:name="uname"
            app:argType="string"
            android:defaultValue="@null"
            app:nullable="false"/>
    </fragment>
    <!-- ...... -->
</navigation>

导航库支持的参数类型包括:

类型app:argType 语法是否支持默认值是否支持 null 值
整数app:argType=“integer”
浮点数app:argType=“float”
长整数app:argType=“long”是 - 默认值必须始终以“L”后缀结尾(例如“123L”)。
布尔值app:argType=“boolean”是 -“true”或“false”
字符串app:argType=“string”
资源引用app:argType=“reference”是 - 默认值必须为“@resourceType/resourceName”格式(例如,“@style/myCustomStyle”)或“0”
自定义 Parcelableapp:argType="",其中 是 Parcelable 的完全限定类名称支持默认值“@null”。不支持其他默认值。
自定义 Serializableapp:argType="",其中 是 Serializable 的完全限定类名称支持默认值“@null”。不支持其他默认值。
自定义 Enumapp:argType="",其中 是 Enum 的完全限定名称是 - 默认值必须与非限定名称匹配(例如,“SUCCESS”匹配 MyEnum.SUCCESS)。

注意:
1. 如果参数传递的是 ParcelableSerializableEnum时,注意所传递的参数类型的类在代码混淆时需要做排除处理;
2. <argument> 可以在 <fragment><action> 标签内声,如果需要通过深层链接传参,务必配置在 <fragment> 标签内声明(更多信息参考:创建隐式深层链接)。

4.4.2 在导航时传递参数

    在导航时,也可以实现在目的地之间传递参数,只需要调用带有参数传递的 NaviCotroller.navigate() 接口即可。

示例:

val navController = findNavController()
navController.navigate(R.id.action_homeFragment_to_loginFragment, Bundle().also {
    it.putInt("type", 2)
})

注意事项:
1. 在导航图的操作中定义的参数,参数值是固定的,但是这个值可以被导航时传递的参数覆盖;
2. 导航的参数传递是单向的,无法实现往回传递,如果需要往回传递参数,可以通过目的地所有者 Activity 进行。

4.5 在目的地之间添加动画效果

    导航支持在目的地之间添加动画,以提高用户体验。导航动画在定义操作是添加,动画包括个类型:

  • app:enterAnim 进入目的地的动画(新的目的地进入的动画)
  • app:exitAnim 退出目的地的动画(新的目的地进入,旧目的地退出的动画)
  • app:popEnterAnim 通过弹出操作进入的目的地的动画(弹出操作时,进入的目的地进入的动画)
  • app:popExitAnim 通过弹出操作退出的目的地的动画(弹出操作时,退出的目的地退出的动画)

示例:

<?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/nav_graph_login"
    app:startDestination="@id/homeFragment">
    <fragment
        android:id="@+id/homeFragment"
        android:name="com.owen.navigationdemo.HomeFragment"
        android:label="HomeFragment"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_loginFragment"
            app:destination="@+id/loginFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
    </fragment>
    <!-- ...... -->
</navigation>

4.5.1 将弹出动画应用于 Activity 目的地过度

    当导航的目的地是 Activity 类型时,在操作中添加的过度动画,会出现进入(打开 Activity)时动画正常,但是返回时(从 Activity 中返回)的动画却失效了。针对目的地是 Activity 类型时,需要对弹出动画做特殊处理,您需要重写 Activityfinish() 方法,在内部调用 ActivityNavigator.applyPopAnimationsToPendingTransition(Activity) 接口即可。
示例:

override fun finish() {
    super.finish()
    ActivityNavigator.applyPopAnimationsToPendingTransition(this)
}

4.6 为目的地创建深层链接

    在 Android 中,深层链接是指将用户直接转到应用内特定目的地的链接。借助导航组件,您可以创建两种不同类型的深层链接:显式深层链接隐式深层链接

4.6.1 创建显式深层链接

    显式深层链接是深层链接的一个实例,该实例使用 PendingIntent 将用户转到应用内的特定位置(例如,可以在通知、应用快捷方式或应用微件中显示显式深层链接)。

    当用户通过显式深层链接打开应用时,任务返回堆栈会被清除,并被替换为相应的深层链接目的地。当嵌套图表时,每个嵌套级别的起始目的地(即层次结构中每个 <navigation> 元素的起始目的地)也会添加到相应堆栈中。也就是说,当用户从深层链接目的地按下返回按钮时,就像从应用入口一步步进入到指定目的地的返回一样的效果。

    创建显式深层链接,可以使用 NavDeepLinkBuilder 类构建 PendingIntent,如下例所示:

val pendingIntent = NavDeepLinkBuilder(requireContext())
    .setGraph(R.navigation.nav_graph)
    .setDestination(R.id.accountSettingFragment)
    .createPendingIntent()

如果已有 NavController,则还可以通过 NavController.createDeepLink() API 创建深层链接,如下所示:

val pendingIntent = findNavController()
    .createDeepLink()
    .setGraph(R.navigation.nav_graph)
    .setDestination(R.id.accountSettingFragment)
    .createPendingIntent()

注意事项:
1. 第一种创建显式深层链接的方式中,如果提供的上下文不是 Activity,构造函数会使用 PackageManager.getLaunchIntentForPackage() 作为默认 Activity 来启动(如果有);
2. 显式深层链接生成的对象是 PendingIntent,适合的场景有通知、快捷方式启动、桌面小部件等。

显式深层链接的使用示例(在通知中使用):

private fun showNotification(context: Context) {
    var notificationManager:NotificationManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        notificationManager = context.getSystemService<NotificationManager>(NotificationManager::class.java)
        if (null != notificationManager) {
            val channel = NotificationChannel("default", "default", NotificationManager.IMPORTANCE_DEFAULT
            )
            notificationManager.createNotificationChannel(channel)
        }
    } else {
        notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    }

    val pendingIntent = findNavController().createDeepLink()
        .setGraph(R.navigation.nav_graph)
        .setDestination(R.id.accountSettingFragment)
        .createPendingIntent()

    val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, "default")
        .setSmallIcon(R.mipmap.ic_launcher)
        .setContentTitle("测试深层链接")
        .setContentText("测试显示深层链接打开应用")
        .setContentIntent(pendingIntent) //                .setVibrate(new long[] { 1000, 1000, 1000, 1000, 1000 })
        .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
        .setAutoCancel(true)


    notificationManager.notify(1, builder.build())
}

4.6.2 创建隐式深层链接

    隐式深层链接是指应用中特定目的地的 URI。调用 URI(例如用户点击某个链接)时,Android 可以将应用打开并自动导航到相应的目的地。

    当用户触发隐式深层链接时,返回堆栈的状态取决于是否使用 Intent.FLAG_ACTIVITY_NEW_TASK 标记启动隐式 Intent

  • 如果该标记已设置,则任务返回堆栈会被清除,并被替换为相应的深层链接指定的目的地。就像显式深层链接,当嵌套图表时,每个嵌套级别的起始目的地(即层次结构中每个 <navigation> 元素的起始目的地)也会添加到相应堆栈中。也就是说,当用户从深层链接目的地按下返回按钮时,就像从应用入口一步步进入到指定目的地的返回一样的效果。
  • 如果该标记未设置,则仍然位于上一个应用的任务堆栈中,该应用中的隐式深层链接已触发。在这种情况下,如果按下返回按钮,则您会返回到上一个应用;如果按下向上按钮,则会在导航图中的层次父级目的地上启动应用的任务。

    创建隐式深层链接请安一下步骤:

第一步:在导航图中添加隐式深层链接声明

    在导航图中添加隐式深层链接声明,只需要在导航图内目的地中添加 <deepLink> 标签,配置相关属性:

<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_graph_login"
    app:startDestination="@id/homeFragment">
    <fragment
        android:id="@+id/homeFragment"
        android:name="com.owen.navigationdemo.HomeFragment"
        android:label="HomeFragment"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_loginFragment"
            app:destination="@+id/loginFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" >
        </action>
        <action
            android:id="@+id/action_home_to_settings"
            app:destination="@+id/Settings"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
    </fragment>

    <fragment
        android:id="@+id/loginFragment"
        android:name="com.owen.navigationdemo.LoginFragment"
        android:label="LoginFragment"
        tools:layout="@layout/fragment_login">
        <action
            android:id="@+id/action_loginFragment_to_registerFragment"
            app:destination="@+id/registerFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"/>
        <argument
            android:name="type"
            app:argType="integer"
            android:defaultValue="0"
            app:nullable="false"/>
        <argument
            android:name="uname"
            app:argType="string"
            android:defaultValue="@null"
            app:nullable="false"/>
        
        <!-- 声明deepLink -->
        <deepLink
            android:id="@+id/settingsDeepLink"
            app:uri="https://www.owen.com"
            android:autoVerify="false"/>
    </fragment>
    <!-- ........  -->
</navigation>

<deepLink>标签属性说明:

  • android:id 深层链接ID
  • app:uri 深层链接Uri
  • android:autoVerify 要求 Google 验证您是相应 URI 的所有者(可选),API 23 开始有效。

创建隐式深层链接需要注意的几点:

  • 没有协议的 URI 会假定为同时支持 http 和 https。例如:www.owen.com,会同时和 http://www.owen.comhttps://www.owen.com 匹配
  • 深层链接的后缀中可以包含形式为 {placeholder_name} 的占位符,用来匹配一个或多个字符。例如,https://www.owen.com/users/{id}https://www.owen.com/users/4 匹配。导航件通过将占位符名称与深层链接所指向的目的地中已定义的参数相匹配,并尝试将占位符值解析为相应的类型。如果目的地中没有定义具有相同名称的参数,则使用默认的 String 类型传参数。
  • 在深层链接的后缀中,可以使用 .* 通配符匹配 0 个或多个字符。
  • 如果目的地参数列表中定义了不能为 null 的参数,则深层链接必须包含该参数且不能为空,否则打开应用会有异常。
第二步:启用导航图中的隐式深层链接

    声明了隐式深层链接,接下来必须启用隐式深层链接,在应用的 AndroidManifest.xml文件中,在导航图所关联的 <activity> 声明中添加 <nav-graph> 元素,如下例所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.owen.navigationdemo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <nav-graph android:value="@navigation/nav_graph" />
        </activity>
        
        <!-- 启用导航图中的隐式深层链接 -->
        <activity android:name=".UploadAvatarActivity" />
    </application>

</manifest>

    构建项目时,导航件会将 <nav-graph> 元素进行转换生成的 <intent-filter> 元素,以匹配导航图中的所有深层链接。

注意事项:
1. 启用隐式深层链接必须在导航图关联的 Activity 声明中进行;
2. <nav-graph> 元素在 Android Studio 3.0 或 3.1 中不支持,使用这些版本时,必须改为手动添加 intent-filter 元素。

第三步:测试隐式深层链接

    隐式深层链接是URI,可以编写一个含有深层链接跳转的 html 文件放到存储中,使用浏览器访问该 html 文件,如下示例:

<!DOCTYPE html>
<head>
    <meta charset="UTF-8" />
    <meta id="viewport" name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,minimal-ui">
</head>
<html>
    <input type="button" value="点击我打开Deeplink" onclick="javascrtpt:window.location.href='https://www.owen.com/'">
</html>

    将以上文件存储到手机存储中,在浏览器的地址栏中输入 file:///<html_path><html_path> 为 html 文件在存储中的路径,例如:file:///sdcard/test_deeplink.html),访问html之后(如下图),点击按钮就可以打开应用并进入到相应的目的地。
deeplin测试html打开效果

注意事项:
1. 如果深层链接中使用的协议头在其他应用中也声明了,打开深层链接系统时可能会弹出应用选择列表,这个问题可以在定义深层链接时,通过定义应用独有的协议头规避;
2. 如果导航组件自动生成的 intent-filter 无法正确进入到目的地,请确认深层链接是否跟自动生成的 intent-filter 一致,如果不一致,可以修改深层链接或者采用手动添加 intent-filter(这个笔者遇到了,https://www.owen.com 的深层链接,生成的 intent-filter 中包含 android:path="/" 导致无法正确访问到指定目的地,修改深层链接为 https://www.owen.com/ 解决了)。

手动添加 intent-filter 元素示例:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.owen.navigationdemo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <!-- 启用导航图中的隐式深层链接 -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="https"
                    android:host="www.owen.com"
                    android:path="/" />
            </intent-filter>
        </activity>

        <activity android:name=".UploadAvatarActivity" />
    </application>

</manifest>

编者语

    导航组件有着非常大的优势,不但使用轻量级的 Fragment,还能使用深层链接打开应用时,能自动构建返回栈(如果是隐式深层链接,启动务必是 Intent.FLAG_ACTIVITY_NEW_TASK)。大家可以在自己的项目中尝试使用导航组件,享受导航组件带来的便利。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐