在日常开发中,越来越多的会使用到一个activity嵌套多个fragment的场景,典型的例子就是app的首页,一般都会由一个activity+多个Fragment组成的底部导航界面,那对于Fragment的显示、隐藏等我们通常都是通过FragmentManager进行管理,但这种方式很容易造成代码臃肿,难以维护。
而通过Jetpack的导航组件——Navigation,就可以很方便的管理各fragment之间的切换,让开发变得更简单。官方文档
Navigation主要由三部分组成:
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"
}
新建好的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>
startDestination
配置默认启动的第一个页面,这里配置的是FragmentAdestination
定义跳转的目标页,还可以定义跳转时的动画等等
<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
指定NavHostFragmentapp:navGraph
指定导航视图,即建好的nav_graph.xmlapp:defaultNavHost=true
意思是可以拦截系统的返回键,可以理解为默认给fragment实现了返回键的功能,这样在fragment的跳转过程中,当我们按返回键时,就可以使得fragment跟activity一样可以回到上一个页面了现在我们运行程序,就可以正常跑起来了,并且看到了FragmentA展示的页面,这是因为MainActivity的布局文件中配置了NavHostFragment,并且给NavHostFragment指定了导航视图,而导航视图中通过startDestination指定了默认展示FragmentA。
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)
}
}
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)
}
}
您可以将一系列目的地归入父级导航图(称为“根图”)内的一个嵌套图中。嵌套图对于整理和重复使用应用界面的各个部分(例如独立的登录流程)非常有用。
如下图显示了一个简单的转帐应用的导航图。从左侧的起始目的地中,该图分出两个流程:一个位于顶部,用于汇款;另一个位于底部,用于查看帐号余额。
如需将目的地归入一个嵌套图中,请执行以下操作:
<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>
findNavController().navigate(R.id.action_mainFragment_to_sendMoneyGraph)
//findNavController().navigate(mainFragmentDirections.actionMainFragmentToSendMoneyGraph)
您可以使用全局操作来创建可由多个目的地共用的通用操作。例如,您可能想要不同目的地中的多个按钮导航到同一应用主屏幕。
如需创建全局操作,请执行以下操作:
<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>
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)
在 Attributes 面板的 Animations 部分中,点击要添加的动画旁边的下拉箭头。您可以从以下类型中进行选择:
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>
通常情况下,强烈建议您仅在目的地之间传递最少量的数据。因为在 Android 上用于保存所有状态的总空间是有限的。如果您需要传递大量数据,不妨考虑使用 ViewModel
传递数据,创建 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")
如需在目的地之间传递数据,首先请按照以下步骤将参数添加到接收它的目的地来定义参数:
点击 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)。 | 否 |
android:defaultValue="@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
点击action即连接线,右侧面板中还可以看到popUpTo
、popUpToInclusive
、launchSingleTop
launchSingleTop
:如果栈中已经包含了指定要跳转的界面,那么只会保留一个,不指定则栈中会出现两个界面相同的Fragment数据,可以理解为类似activity的 singleTop,即栈顶复用模式popUpTo(tag)
:表示跳转到某个tag,并将tag之上的元素出栈。popUpToInclusive
:为true表示会弹出tag,false则不会例子:FragmentA -> FragmentB -> FragmentC -> FragmentA
设置FragmentC -> FragmentA 的action为popUpTo=FragmentA
,popUpToInclusive=false
,那么栈内元素变化为
最后会发现需要按两次返回键才会回退到桌面