Skip to content

Latest commit

 

History

History
509 lines (336 loc) · 25.2 KB

12.Navigation简介.md

File metadata and controls

509 lines (336 loc) · 25.2 KB

12.Navigation简介

单个Activity嵌套多个Fragment的UI架构模式,已经被大多数Android工程师所接受和采用。但是,对Fragment的管理一直是一件比较麻烦的事情。工程师需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。页面的切换通常还包括对应用程序App bar的管理、Fragment间的切换动画,以及Fragment间的参数传递。纯代码的方式使用起来不是特别友好,并且Fragment和App bar在管理和使用的过程中显得很混乱。

为此,Jetpack提供了一个名为Navigation的组件,旨在方便我们管理页面和App bar。

Navigation 是一个框架,用于在 Android 应用中的“目的地”之间导航,该框架提供一致的 API,无论目的地是作为 fragment、activity 还是其他组件实现。

它具有以下优势:

  • 可视化的页面导航图,类似于Apple Xcode中的StoryBoard,便于我们理清页面间的关系。
  • 通过destination和action完成页面间的导航。
  • 方便添加页面切换动画。
  • 页面间类型安全的参数传递。
  • 通过NavigationUI类,对菜单、底部导航、抽屉菜单导航进行统一的管理。
  • 支持深层链接DeepLink。

Navigation 组件旨在用于具有一个主 Activity 和多个 Fragment 目的地的应用。主 Activity 与导航图相关联,且包含一个负责根据需要交换目的地的 NavHostFragment。在具有多个 Activity 目的地的应用中,每个 Activity 均拥有其自己的导航图。

在使用过程中,我们感受到如下的优点。

  • 页面跳转性能更好,在单 Activity 的架构下,都是 fragment 的切换,每次 fragment 被压栈之后,View 被销毁,相比之前 Activity 跳转,更加轻量,需要的内存更少。
  • 通过 Viewmodel 进行数据共享更便捷,不需要页面之间来回传数据。
  • 统一的 Navigation API 来更精细的控制跳转逻辑。

依赖

如果想要使用Navigation,需要现在build.gradle文件中添加以下依赖:

dependencies {
  def nav_version = "2.3.5"

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

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

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

  // Jetpack Compose Integration
  implementation "androidx.navigation:navigation-compose:1.0.0-alpha10"
}

Navigation的主要元素

导航组件由以下三个关键部分组成:

  1. Navigation Graph : 图表

    一种数据结果,用于定义应用中的所有导航目的地以及它们如何连接在一起。 在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为目标)以及用户可以通过应用获取的可能路径。

  2. NavHost : 主机

    显示导航图中目标的空白容器。导航组件包含一个默认NavHost实现 (NavHostFragment),可显示Fragment目标。

  3. NavController : 控制器

    在NavHost中管理应用导航的对象。当用户在整个应用中移动时,NavController会安排NavHost中目标内容的交换。
    该控制器提供了一些方法,可用于在目的地之间导航、处理深层链接、管理返回堆栈等。

在应用中导航时,您告诉NavController,您想沿导航图中的特定路径导航至特定目标,或直接导航至特定目标。NavController便会在NavHost中显示相应目标。

Navigation Graph(导航图)

导航图是一种资源文件,其中包含您的所有目的地和操作。该图表会显示应用的所有导航路径。

如需向项目添加导航图,请执行以下操作:

  1. 在“Project”窗口中,右键点击 res 目录,然后依次选择 New > Android Resource File。此时系统会显示 New Resource File 对话框。
  2. File name 字段中输入名称,例如“nav_graph”。
  3. Resource type 下拉列表中选择 Navigation,然后点击 OK
<?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"
            android:id="@+id/nav_graph">
</navigation>

<navigation> 元素是导航图的根元素。当您向图表添加目的地和连接操作时,可以看到相应的 <destination><action> 元素在此处显示为子元素。如果您有嵌套图表,它们将显示为子 <navigation> 元素。

向 Activity 添加 NavHost

导航宿主是 Navigation 组件的核心部分之一。导航宿主是一个空容器,用户在您的应用中导航时,目的地会在该容器中交换进出。

导航宿主必须派生于 NavHost。Navigation 组件的默认 NavHost 实现 (NavHostFragment) 负责处理 Fragment 目的地的交换。

通过 XML 添加 NavHostFragment

以下 XML 示例显示了作为应用主 Activity 一部分的 NavHostFragment

<?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">

    <androidx.appcompat.widget.Toolbar
        .../>

    <androidx.fragment.app.FragmentContainerView
        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/nav_graph" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        .../>

</androidx.constraintlayout.widget.ConstraintLayout>

NavHostFragment是一个特殊的Fragment,我们需要将其添加到Activity的布局文件中,作为其他Fragment的容器。

请注意以下几点:

  • android:name 属性包含 NavHost 实现的类名称。
  • app:navGraph 属性将 NavHostFragment 与导航图相关联。导航图会在此 NavHostFragment 中指定用户可以导航到的所有目的地。
  • app:defaultNavHost="true" 属性确保您的 NavHostFragment 会自动处理系统返回键,即当用户按下手机的返回按钮时,系统能自动将当前所展示的Fragment退出。请注意,只能有一个默认 NavHost。如果同一布局(例如,双窗格布局)中有多个宿主,请务必仅指定一个默认 NavHost
<?xml version="1.0" encoding="utf-8"?>
<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/blankFragment"> // 起始fragment
    <fragment
        android:id="@+id/blankFragment"
        android:name="com.example.cashdog.cashdog.BlankFragment"
        android:label="Blank"
        tools:layout="@layout/fragment_blank" />
</navigation>

Action

<?xml version="1.0" encoding="utf-8"?>
<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/blankFragment">
    <fragment
        android:id="@+id/blankFragment"
        android:name="com.example.cashdog.cashdog.BlankFragment"
        android:label="fragment_blank"
        tools:layout="@layout/fragment_blank" >
        <action
            android:id="@+id/action_blankFragment_to_blankFragment2"
            app:destination="@id/blankFragment2" />
    </fragment>
    <fragment
        android:id="@+id/blankFragment2"
        android:name="com.example.cashdog.cashdog.BlankFragment2"
        android:label="fragment_blank_fragment2"
        tools:layout="@layout/fragment_blank_fragment2" />
</navigation>

在导航图中,操作由 <action> 元素表示。操作至少应包含自己的 ID 和用户应转到的目的地的 ID。

导航到目的地

导航到目的地是使用 NavController 完成的,它是一个在 NavHost 中管理应用导航的对象。每个 NavHost 均有自己的相应 NavController。您可以使用以下方法之一检索 NavController

Kotlin

Java

使用 FragmentContainerView 创建 NavHostFragment,或通过 FragmentTransaction 手动将 NavHostFragment 添加到您的 Activity 时,尝试通过 Navigation.findNavController(Activity, @IdRes int) 检索 Activity 的 onCreate() 中的 NavController 将失败。您应改为直接从 NavHostFragment 检索 NavController

val navHostFragment =
        supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
navController.navigate(R.id.action_blankFragment_to_blankFragment2)

对于按钮,您还可以使用 Navigation 类的 createNavigateOnClickListener() 便捷方法导航到目的地,如下例所示:

button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next_fragment, null))

使用 DeepLinkRequest 导航

您可以使用 navigate(NavDeepLinkRequest) 直接导航到隐式深层链接目的地,如下例所示:

val request = NavDeepLinkRequest.Builder
    .fromUri("android-app://androidx.navigation.app/profile".toUri())
    .build()
findNavController().navigate(request)

导航和返回堆栈

Android 会维护一个返回堆栈,其中包含您之前访问过的目的地。当用户打开您的应用时,应用的第一个目的地就放置在堆栈中。每次调用 navigate() 方法都会将另一目的地放置到堆栈的顶部。点按向上返回会分别调用 NavController.navigateUp()NavController.popBackStack() 方法,用于移除(或弹出)堆栈顶部的目的地。

NavController.popBackStack() 会返回一个布尔值,表明它是否已成功返回到另一个目的地。当返回 false 时,最常见的情况是手动弹出图的起始目的地。

如果该方法返回 false,则 NavController.getCurrentDestination() 会返回 null。您应负责导航到新目的地,或通过对 Activity 调用 finish() 来处理弹出情况,如下例所示:

if (!navController.popBackStack()) {
    // Call finish() on your Activity
    finish()
}

popUpTo 和 popUpToInclusive

使用操作进行导航时,您可以选择从返回堆栈上弹出其他目的地。例如,如果您的应用具有初始登录流程,那么在用户登录后,您应将所有与登录相关的目的地从返回堆栈上弹出,这样返回按钮就不会将用户带回登录流程。

如需在从一个目的地导航到另一个目的地时弹出目的地,请在关联的 <action> 元素中添加 app:popUpTo 属性。app:popUpTo 会告知 Navigation 库在调用 navigate() 的过程中从返回堆栈上弹出一些目的地。属性值是应保留在堆栈中的最新目的地的 ID。

您还可以添加 app:popUpToInclusive="true",以表明在 app:popUpTo 中指定的目的地也应从返回堆栈中移除。

通过 引用其他导航图

在导航图中,您可以使用 include 引用其他图。虽然这在功能上与使用嵌套图相同,但 include 可让您使用其他项目模块或库项目中的图,如以下示例所示:

<!-- (root) nav_graph.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/fragment">

    <include app:graph="@navigation/included_graph" />

    <fragment
        android:id="@+id/fragment"
        android:name="com.example.myapplication.BlankFragment"
        android:label="Fragment in Root Graph"
        tools:layout="@layout/fragment_blank">
        <action
            android:id="@+id/action_fragment_to_second_graph"
            app:destination="@id/second_graph" />
    </fragment>

    ...
</navigation>
<!-- included_graph.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/second_graph"
    app:startDestination="@id/includedStart">

    <fragment
        android:id="@+id/includedStart"
        android:name="com.example.myapplication.IncludedStart"
        android:label="fragment_included_start"
        tools:layout="@layout/fragment_included_start" />
</navigation>

创建全局操作

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

<?xml version="1.0" encoding="utf-8"?>
<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>

如需在代码中使用某个全局操作,请将该全局操作的资源 ID 传递到每个界面元素的 navigate() 方法,如以下示例所示:

viewTransactionButton.setOnClickListener { view ->
    view.findNavController().navigate(R.id.action_global_mainFragment)
}

使用 Safe Args 实现类型安全的导航

如需在目的地之间导航,建议使用 Safe Args Gradle 插件。此插件可生成简单的对象和构建器类,以便在目的地之间实现类型安全的导航。我们强烈建议您在导航以及在目的地之间传递数据时使用 Safe Args。

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

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

您还必须应用以下两个可用插件之一。

如需生成适用于 Java 模块或 Java 和 Kotlin 混合模块的 Java 语言代码,请将以下行添加到应用或模块build.gradle 文件中:

apply plugin: "androidx.navigation.safeargs"

此外,如需生成适用于 Kotlin 独有的模块的 Kotlin 代码,请添加以下行:

apply plugin: "androidx.navigation.safeargs.kotlin"

根据迁移到 AndroidX) 文档,您的 gradle.properties 文件中必须具有 android.useAndroidX=true

启用 Safe Args 后,生成的代码会包含已定义的每个操作的类和方法,以及与每个发送目的地和接收目的地相对应的类。

Safe Args 为生成操作的每个目的地生成一个类。生成的类名称会在源目的地类名称的基础上添加“Directions”。例如,如果源目的地的名称为 SpecifyAmountFragment,则生成的类的名称为 SpecifyAmountFragmentDirections

生成的类为源目的地中定义的每个操作提供了一个静态方法。该方法接受任何定义的操作参数为参数,并返回可直接传递到 navigate()NavDirections 对象。

Safe Args 示例

例如,假设我们的导航图包含一个操作,该操作将两个目的地 SpecifyAmountFragmentConfirmationFragment 连接起来。ConfirmationFragment 接受您作为操作的一部分提供的单个 float 参数。

Safe Args 会生成一个 SpecifyAmountFragmentDirections 类,其中只包含一个 actionSpecifyAmountFragmentToConfirmationFragment() 方法和一个名为 ActionSpecifyAmountFragmentToConfirmationFragment 的内部类。这个内部类派生自 NavDirections 并存储了关联的操作 ID 和 float 参数。然后,您可以将返回的 NavDirections 对象直接传递到 navigate(),如下例所示:

override fun onClick(v: View) {
    val amount: Float = ...
    val action =
        SpecifyAmountFragmentDirections
            .actionSpecifyAmountFragmentToConfirmationFragment(amount)
    v.findNavController().navigate(action)
}

传递参数

Navigation 支持您通过定义目的地参数将数据附加到导航操作。例如,用户个人资料目的地可能会根据用户 ID 参数来确定要显示哪个用户。

通常情况下,强烈建议您仅在目的地之间传递最少量的数据。例如,您应该传递键来检索对象而不是传递对象本身,因为在 Android 上用于保存所有状态的总空间是有限的。如果您需要传递大量数据,不妨考虑使用 ViewModel(如在 Fragment 之间共享数据中所述)。

<action android:id="@+id/startMyFragment"
    app:destination="@+id/myFragment">
    <argument
        android:name="myArg"
        app:argType="integer"
        android:defaultValue="1" />
</action>

通过声明argement节点来指定参数。

启用 Safe Args 后,生成的代码会为每个操作包含以下类型安全的类和方法,以及每个发送和接收目的地。

  • 为生成操作的每一个目的地创建一个类。该类的名称是在源目的地的名称后面加上“Directions”。例如,如果源目的地是名为 SpecifyAmountFragment 的 Fragment,则生成的类的名称为 SpecifyAmountFragmentDirections

    该类会为源目的地中定义的每个操作提供一个方法。

  • 对于用于传递参数的每个操作,都会创建一个 inner 类,该类的名称根据操作的名称确定。例如,如果操作名称为 confirmationAction,,则类名称为 ConfirmationAction。如果您的操作包含不带 defaultValue 的参数,则您可以使用关联的 action 类来设置参数值。

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

override fun onClick(v: View) {  val amountTv: EditText = view!!.findViewById(R.id.editTextAmount)  val amount = amountTv.text.toString().toInt()  val action = SpecifyAmountFragmentDirections.confirmationAction(amount)  v.findNavController().navigate(action)}

在接收目的地的代码中,请使用 getArguments() 方法来检索 bundle 并使用其内容。使用 -ktx 依赖项时,Kotlin 用户还可以使用 by navArgs() 属性委托来访问参数。

val args: ConfirmationFragmentArgs by navArgs()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val tv: TextView = view.findViewById(R.id.textViewAmount)
    val amount = args.amount
    tv.text = amount.toString()
}

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

如果您不使用 Gradle,仍然可以使用 Bundle 对象在目的地之间传递参数。创建 Bundle 对象并使用 navigate() 将它传递给目的地,如下所示:

val bundle = bundleOf("amount" to amount)view.findNavController().navigate(R.id.confirmationAction, bundle)

在接收目的地的代码中,请使用 getArguments() 方法来检索 Bundle 并使用其内容:

val tv = view.findViewById<TextView>(R.id.textViewAmount)
tv.text = arguments?.getString("amount")

NavigationUI

导航图是Navigation组件中很重要的一部分,它可以帮助我们快速了解页面之间的关系,再通过NavController便可以完成页面的切换工作。而在页面的切换过程中,通常还伴随着App bar中menu菜单的变化。对于不同的页面,App bar中的menu菜单很可能是不一样的。App bar中的各种按钮和菜单,同样承担着页面切换的工作。例如,当ActionBar左边的返回按钮被单击时,我们需要响应该事件,返回到上一个页面。既然Navigation和App bar都需要处理页面切换事件,那么,为了方便管理,Jetpack引入了NavigationUI组件,使App bar中的按钮和菜单能够与导航图中的页面关联起来。

NavigationUI 支持以下顶部应用栏类型:

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    val navHostFragment =
        supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
    val navController = navHostFragment.navController
    val appBarConfiguration = AppBarConfiguration(
        topLevelDestinationIds = setOf(),
        fallbackOnNavigateUpListener = ::onSupportNavigateUp
    )
    findViewById<Toolbar>(R.id.toolbar)
        .setupWithNavController(navController, appBarConfiguration)
}

参考

https://mp.weixin.qq.com/s?src=11&timestamp=1712714064&ver=5191&signature=JTMgHGLtMGW*NoSWSrLNVuGzs-KEEDznO-ja7*X*KumZMFAuIRl7WbPYT1gG7AX810nUx6Ftb6nm6Ao92M*GzojPfqBUo1wOFc0gMs1mseTLkUWZ9Q*BIW69MM7ULPDV&new=1