Android Navigation 组件(基础篇)

一、前言

在日常开发中,越来越多的会使用到一个activity嵌套多个fragment的场景,典型的例子就是app的首页,一般都会由一个activity+多个Fragment组成的底部导航界面,那对于Fragment的显示、隐藏等我们通常都是通过FragmentManager进行管理,但这种方式很容易造成代码臃肿,难以维护。
而通过Jetpack的导航组件——Navigation,就可以很方便的管理各fragment之间的切换,让开发变得更简单。官方文档

Navigation主要由三部分组成:

  • Navigation graph:一个包含所有导航相关信息的 XML 资源
  • NavHostFragment:一种特殊的Fragment,用于承载导航内容的容器
  • NavController:管理应用导航的对象,实现Fragment之间的跳转等操作

二、基本使用

2.1 添加依赖

dependencies {
  def nav_version = "2.3.1"

  // 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"
}

2.2 创建导航图

  1. 在“Project”窗口中,右键点击 res 目录,然后依次选择 New > Android Resource File。此时系统会显示 New Resource File 对话框。
  2. 在 File name 字段中输入名称,例如“nav_graph”。
  3. 从 Resource type 下拉列表中选择 Navigation,然后点击 OK。

Android Navigation 组件(基础篇)_第1张图片

2.3 Navigation graph

新建好的nav_graph.xml切换到design模式下,在 Navigation Editor 中,点击 New Destination 图标,然后点击 Create new destination,即可快速创建新的Fragment,这里分别新建了Fragme0ntA、FragmentB、FragmentC三个fragment


可通过手动配置页面之间的跳转关系,点击某个页面,右边会出现一个小圆点,拖曳小圆点指向跳转的页面,这里设置跳转的关系为FragmentA -> FragmentB -> FragmentC。

切换到Code栏,可以看到生成了如下代码


<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/fragmentA">

    <fragment
        android:id="@+id/fragmentA"
        android:name="com.example.testnavigation.FragmentA"
        android:label="fragment_a"
        tools:layout="@layout/fragment_a" >
        <action
            android:id="@+id/action_fragmentA_to_fragmentB"
            app:destination="@id/fragmentB" />
    fragment>
    <fragment
        android:id="@+id/fragmentB"
        android:name="com.example.testnavigation.FragmentB"
        android:label="fragment_b"
        tools:layout="@layout/fragment_b" >
        <action
            android:id="@+id/action_fragmentB_to_fragmentC"
            app:destination="@id/fragmentC" />
    fragment>
    <fragment
        android:id="@+id/fragmentC"
        android:name="com.example.testnavigation.FragmentC"
        android:label="fragment_c"
        tools:layout="@layout/fragment_c" />
navigation>

  • navigation是根标签,通过startDestination配置默认启动的第一个页面,这里配置的是FragmentA
  • fragment标签代表一个fragment
  • action标签定义了页面跳转的行为,相当于上图中的每条线,destination定义跳转的目标页,还可以定义跳转时的动画等等

2.4 NavHostFragment


<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"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/nav_graph" />

androidx.constraintlayout.widget.ConstraintLayout>

  • android:name指定NavHostFragment
  • app:navGraph指定导航视图,即建好的nav_graph.xml
  • app:defaultNavHost=true 意思是可以拦截系统的返回键,可以理解为默认给fragment实现了返回键的功能,这样在fragment的跳转过程中,当我们按返回键时,就可以使得fragment跟activity一样可以回到上一个页面了

现在我们运行程序,就可以正常跑起来了,并且看到了FragmentA展示的页面,这是因为MainActivity的布局文件中配置了NavHostFragment,并且给NavHostFragment指定了导航视图,而导航视图中通过startDestination指定了默认展示FragmentA。

2.5 NavController

NavController用来管理fragment之间的跳转
每个 NavHost 均有自己的相应 NavController。您可以使用以下方法之一检索 NavController
Kotlin:

  • Fragment.findNavController()
  • View.findNavController()
  • Activity.findNavController(viewId: Int)

Java:

  • NavHostFragment.findNavController(Fragment)
  • Navigation.findNavController(Activity, @IdRes int viewId)
  • Navigation.findNavController(View)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    tv.setOnClickListener {
        val navController = findNavController()
        navController.navigate(R.id.action_fragmentA_to_fragmentB)
    }
}

2.6 使用 Safe Args 确保类型安全

fragment之间的跳转上面已经可以实现了,但是Google建议使用 Safe Args Gradle 插件实现。该插件可以生成简单的对象和构建器类,这些类支持在目的地之间进行类型安全的导航和参数传递。

如需将 Safe Args 添加到您的项目,请在顶层 build.gradle 文件中包含以下 classpath

buildscript {
    repositories {
        google()
    }
    dependencies {
        def nav_version = "2.3.1"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

请将以下行添加到应用或模块的 build.gradle 文件中:

//适用于 Java 模块或 Java 和 Kotlin 混合模块的 Java 语言代码
// apply plugin: "androidx.navigation.safeargs"

//仅 Kotlin 模块的 Kotlin 语言代码
apply plugin: "androidx.navigation.safeargs.kotlin"

启用 Safe Args 后,该插件会生成代码,其中包含您定义的每个操作的类和方法。生成的类的名称由源目的地类的名称(即Fragment类名称)和“Directions”一词组成。

例如,上面的导航图中,源目的地 fragmentA跳转到接收目的地 fragmentB。Safe Args 会生成一个 fragmentADirections类,其中只包含一个 actionFragmentAToFragmentB() 方法(该方法会返回 NavDirections 对象)。然后,您可以将返回的 NavDirections 对象直接传递到 navigate(),如以下示例所示:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    tv.setOnClickListener {
        val action = fragmentADirections.actionFragmentAToFragmentB()
        findNavController().navigate(action)
    }
}

三、更多操作

3.1 嵌套导航图

您可以将一系列目的地归入父级导航图(称为“根图”)内的一个嵌套图中。嵌套图对于整理和重复使用应用界面的各个部分(例如独立的登录流程)非常有用。

如下图显示了一个简单的转帐应用的导航图。从左侧的起始目的地中,该图分出两个流程:一个位于顶部,用于汇款;另一个位于底部,用于查看帐号余额。
Android Navigation 组件(基础篇)_第2张图片

如需将目的地归入一个嵌套图中,请执行以下操作:

  1. 在 Navigation Editor 中,按住 Shift 键,然后点击您想要添加到嵌套图中的目的地。
  2. 右键点击以打开上下文菜单,然后依次选择 Move to Nested Graph > New Graph。目的地包含在嵌套图中。
    Android Navigation 组件(基础篇)_第3张图片
  3. 双击嵌套图以显示其目的地。
  4. 点击 Text 标签页,以切换到 XML 视图。一个嵌套导航图已添加到该图中

<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    app:startDestination="@id/mainFragment">
    <fragment
        android:id="@+id/mainFragment"
        android:name="com.example.cashdog.cashdog.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main" >
        <action
            android:id="@+id/action_mainFragment_to_sendMoneyGraph"
            app:destination="@id/sendMoneyGraph" />
        <action
            android:id="@+id/action_mainFragment_to_viewBalanceFragment"
            app:destination="@id/viewBalanceFragment" />
    fragment>
    <fragment
        android:id="@+id/viewBalanceFragment"
        android:name="com.example.cashdog.cashdog.ViewBalanceFragment"
        android:label="fragment_view_balance"
        tools:layout="@layout/fragment_view_balance" />
    <navigation android:id="@+id/sendMoneyGraph" app:startDestination="@id/chooseRecipient">
        <fragment
            android:id="@+id/chooseRecipient"
            android:name="com.example.cashdog.cashdog.ChooseRecipient"
            android:label="fragment_choose_recipient"
            tools:layout="@layout/fragment_choose_recipient">
            <action
                android:id="@+id/action_chooseRecipient_to_chooseAmountFragment"
                app:destination="@id/chooseAmountFragment" />
        fragment>
        <fragment
            android:id="@+id/chooseAmountFragment"
            android:name="com.example.cashdog.cashdog.ChooseAmountFragment"
            android:label="fragment_choose_amount"
            tools:layout="@layout/fragment_choose_amount" />
    navigation>
navigation>
  1. 跳转方法相同
 findNavController().navigate(R.id.action_mainFragment_to_sendMoneyGraph)
//findNavController().navigate(mainFragmentDirections.actionMainFragmentToSendMoneyGraph)

3.2 全局操作

您可以使用全局操作来创建可由多个目的地共用的通用操作。例如,您可能想要不同目的地中的多个按钮导航到同一应用主屏幕。

如需创建全局操作,请执行以下操作:

  1. 在 Graph Editor 中,点击一个目的地,使其突出显示。
  2. 右键点击该目的地,以显示上下文菜单。
  3. 依次选择 Add Action > Global。此时系统会在该目的地左侧显示一个箭头。
  4. 点击 Text 标签页,以转到 XML 文本视图。全局操作的 XML 文本大致如下所示:

<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
            xmlns:tools="http://schemas.android.com/tools"
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/main_nav"
            app:startDestination="@id/mainFragment">

  ...

  <action android:id="@+id/action_global_mainFragment"
          app:destination="@id/mainFragment"/>

navigation>
  1. 跳转方法相同
 findNavController().navigate(R.id.action_global_mainFragment)

将 Safe Args 用于全局操作时,您必须为根 元素提供一个 android:id 值,如以下示例中所示:


<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
            xmlns:tools="http://schemas.android.com/tools"
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/nav_graph"
            app:startDestination="@id/mainFragment">

    ...
navigation>

Navigation 会根据 android:id 值为 < navigation > 元素生成一个 Directions 类。

 findNavController().navigate(NavigationDirections.actionGlobalMainFragment)

3.3 添加动画

Attributes 面板的 Animations 部分中,点击要添加的动画旁边的下拉箭头。您可以从以下类型中进行选择:
Android Navigation 组件(基础篇)_第4张图片

  • enterAnim: 跳转时的目标页面动画
  • exitAnim: 跳转时的原页面动画
  • popEnterAnim: 回退时的目标页面动画
  • popExitAnim:回退时的原页面动画

配置动画后会发现action多了四个动画相关的属性

<fragment
        android:id="@+id/fragmentA"
        android:name="com.example.testnavigation.FragmentA"
        android:label="fragment_a"
        tools:layout="@layout/fragment_a" >
        <action
            android:id="@+id/action_fragmentA_to_fragmentB"
            app:destination="@id/fragmentB"
            app:enterAnim="@anim/enter_in"
            app:exitAnim="@anim/enter_out"
            app:popEnterAnim="@anim/exit_in"
            app:popExitAnim="@anim/exit_out" />
fragment>

3.4 传递参数

通常情况下,强烈建议您仅在目的地之间传递最少量的数据。因为在 Android 上用于保存所有状态的总空间是有限的。如果您需要传递大量数据,不妨考虑使用 ViewModel

3.4.1 使用 Bundle 对象在目的地之间传递参数

传递数据,创建 Bundle 对象并使用 navigate() 将它传递给目的地

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    tv.setOnClickListener {
    	val bundle = Bundle()
        bundle.putString("key", "from fragmentA")
        findNavController().navigate(R.id.action_fragmentA_to_fragmentB, bundle))
    }
}

接收数据,getArguments() 方法来检索 Bundle 并使用其内容:

	val keyStr = arguments?.getString("key")

3.4.2 使用 Safe Args 传递安全的数据

如需在目的地之间传递数据,首先请按照以下步骤将参数添加到接收它的目的地来定义参数:

  1. 在 Navigation Editor 中,点击接收参数的目的地。
  2. Attributes 面板中,点击 Add (+)。
  3. 在显示的 Add Argument Link 窗口中,输入参数名称、参数类型、参数是否可为 null,以及默认值(如果需要)。
  4. 点击 Add。请注意,该参数现在会显示在 Attributes 面板的 Arguments 列表中。


点击 Text 标签页以切换到 XML 视图,就会发现您的参数已添加到接收该参数的目的地。相关示例如下所示:

 <fragment android:id="@+id/fragmentB" >
     <argument
         android:name="key"
         app:argType="String"
         android:defaultValue="test" />
 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”
自定义 Parcelable app:argType="< type>",其中 < type> 是 Parcelable 的完全限定类名称 支持默认值“@null”。不支持其他默认值。
自定义 Serializable app:argType="< type>",其中 < type> 是 Serializable 的完全限定类名称 支持默认值“@null”。不支持其他默认值。
自定义 Enum app:argType="< type>",其中 < type> 是 Enum 的完全限定名称 是 - 默认值必须与非限定名称匹配(例如,“SUCCESS”匹配 MyEnum.SUCCESS)。
  • 如果参数类型支持 null 值,您可以使用 android:defaultValue="@null" 声明默认值 null。
  • 如果您选择其中一种自定义类型,则系统会显示 Select Class 对话框,提示您选择与该类型对应的类。您可以通过 Project 标签页从当前项目中选择类。
  • 您可以选择 < inferred type>,让 Navigation 库根据提供的值来确定类型。
  • 您可以选中 Array,以指明参数应该是所选 Type 值的数组。请注意,不支持枚举数组和资源引用数组。不论基础类型的 null 性如何,数组始终可为 null。数组仅支持一个默认值,即“@null”。数组不支持其他任何默认值。

传递数据

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    tv.setOnClickListener {
    	val action = fragmentADirections.actionFragmentAToFragmentB("from fragmentA")
        findNavController().navigate(action)
    }
}

接收数据

Safe Args为接收目的地创建一个类。该类的名称是在目的地的名称后面加上“Args”。例如,如果目的地 Fragment 的名称为 fragmentB ,,则生成的类的名称为 fragmentBArgs。可以使用该类的 fromBundle() 方法检索参数。

	val args: fragmentBArgs by navArgs()
	val keyStr = args.key

3.5 栈管理

点击action即连接线,右侧面板中还可以看到popUpTopopUpToInclusivelaunchSingleTop
Android Navigation 组件(基础篇)_第5张图片

  • launchSingleTop:如果栈中已经包含了指定要跳转的界面,那么只会保留一个,不指定则栈中会出现两个界面相同的Fragment数据,可以理解为类似activity的 singleTop,即栈顶复用模式
  • popUpTo(tag):表示跳转到某个tag,并将tag之上的元素出栈。
  • popUpToInclusive:为true表示会弹出tag,false则不会

例子:FragmentA -> FragmentB -> FragmentC -> FragmentA
设置FragmentC -> FragmentA 的action为popUpTo=FragmentApopUpToInclusive=false,那么栈内元素变化为
Android Navigation 组件(基础篇)_第6张图片最后会发现需要按两次返回键才会回退到桌面

设置popUpToInclusive=true时,栈内元素变化为
Android Navigation 组件(基础篇)_第7张图片
此时只需要按一次返回键就回退到桌面了。

你可能感兴趣的:(Android)