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的主要元素
导航组件由以下三个关键部分组成:
Navigation Graph : 图表
一种数据结果,用于定义应用中的所有导航目的地以及它们如何连接在一起。 在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为目标)以及用户可以通过应用获取的可能路径。
NavHost : 主机
显示导航图中目标的空白容器。导航组件包含一个默认NavHost实现 (NavHostFragment),可显示Fragment目标。
NavController : 控制器
在NavHost中管理应用导航的对象。当用户在整个应用中移动时,NavController会安排NavHost中目标内容的交换。
该控制器提供了一些方法,可用于在目的地之间导航、处理深层链接、管理返回堆栈等。
在应用中导航时,您告诉NavController,您想沿导航图中的特定路径导航至特定目标,或直接导航至特定目标。NavController便会在NavHost中显示相应目标。
Navigation Graph(导航图)
导航图是一种资源文件,其中包含您的所有目的地和操作。该图表会显示应用的所有导航路径。
如需向项目添加导航图,请执行以下操作:
- 在“Project”窗口中,右键点击
res
目录,然后依次选择 New > Android Resource File。此时系统会显示 New Resource File 对话框。 - 在 File name 字段中输入名称,例如“nav_graph”。
- 从 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:
Fragment.findNavController()
View.findNavController()
.findNavController())Activity.findNavController(viewId: Int)
Java:
NavHostFragment.findNavController(Fragment)
)Navigation.findNavController(Activity, @IdRes int viewId)
)Navigation.findNavController(View)
)
使用 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 示例
例如,假设我们的导航图包含一个操作,该操作将两个目的地 SpecifyAmountFragment
和 ConfirmationFragment
连接起来。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)
}
参考
- 邮箱 :[email protected]
- Good Luck!