【Android Jetpack】Navigation的使用
引入
单个Activity嵌套多个Fragment的UI架构模式,非常非常普遍。但是,对Fragment的管理一直是一件比较麻烦的事情。工程师需要通过
FragmentManager
和FragmentTransaction
来管理Fragment之间的切换。页面的切换通常还包括对应用程序App bar的管理、Fragment间的切换动画,以及Fragment间的参数传递。纯代码的方式使用起来不是特别友好,并且Fragment和App bar在管理和使用的过程中显得很混乱。
为此,Jetpack提供了一个名为Navigation
的组件,旨在方便我们管理页面和App bar。
Navigation具有以下优势:
- 可视化的页面导航图,类似于Apple Xcode中的StoryBoard,便于我们理清页面间的关系。
- 通过destination和action完成页面间的导航。
- 方便添加页面切换动画。
- 页面间类型安全的参数传递。
- 通过
NavigationUI
类,对菜单、底部导航、抽屉菜单导航进行统一的管理。 - 支持深层链接DeepLink。
Navigation 组件旨在用于具有一个主 Activity 和多个 Fragment 目的地的应用。主 Activity 与导航图相关联,且包含一个负责根据需要交换目的地的 NavHostFragment
。在具有多个 Activity 目的地的应用中,每个 Activity 均拥有其自己的导航图。
主要元素
导航组件由以下三个关键部分组成:
-
Navigation Graph
在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为目标)以及用户可以通过应用获取的可能路径。
-
NavHost
显示导航图中目标的空白容器。导航组件包含一个默认NavHost实现 (
NavHostFragment
),可显示Fragment目标。 -
NavController
在NavHost中管理应用导航的对象。当用户在整个应用中移动时,NavController会安排NavHost中目标内容的交换。
在应用中导航时,您告诉NavController
,您想沿导航图中的特定路径导航至特定目标,或直接导航至特定目标。NavController
便会在NavHost
中显示相应目标。
例如利用 BottomNavigationView
+ Fragment
/FragmentContainerView
的组合实现底部导航栏
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/btmnv"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@drawable/jianbian0"
app:itemRippleColor="@color/colorDeep"
app:itemIconTint="@drawable/btmnv_select"
app:itemTextColor="@drawable/btmnv_select"
app:menu="@menu/btmnvmenu"
app:labelVisibilityMode="selected"
app:itemBackground="@null"
/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/mobile_navigation" />
代码:
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
binding.btmnv.setupWithNavController(navHostFragment.navController)
Navigation Graph
导航图是一种资源文件,其中包含您的所有目的地和操作。该图表会显示应用的所有导航路径。
向项目添加导航图:
<?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 的更换。
静态添加
<androidx.fragment.app.FragmentContainerView
.....
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
NavHostFragment
是一个特殊的Fragment(android:name所定义的),我们需要将其添加到Activity的布局文件中,作为其他Fragment的容器,然后定义Fragment。
android:name
:包含NavHost
实现的类名称。app:navGraph
:将NavHostFragment
与导航图
相关联。导航图会在此NavHostFragment
中指定用户可以浏览的碎片。app:defaultNavHost="true"
:确保您的NavHostFragment
会自动处理系统返回键,即当用户按下手机的返回按钮时,系统能自动将当前所展示的Fragment退出。并且只能有一个默认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/theFirstFragment"> // 最开始所展示的Fragment
<fragment
android:id="@+id/theFirstFragment"
...... />
</navigation>
导航到目的地
导航到目的地是使用 NavController
完成的,它是一个在 NavHost
中管理应用导航的对象。每个 NavHost
均有自己的相应 NavController
。您可以使用以下方法之一检索 NavController
:
Kotlin:
Fragment.findNavController()
View.findNavController()
Activity.findNavController(viewId: Int)
Java:
NavHostFragment.findNavController(Fragment)
- [
Navigation.findNavController(Activity, @IdRes int viewId)
](https://developer.android.com/reference/androidx/navigation/Navigation#findNavController(android.app.Activity, int)) 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.theAimFragment, 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()
}
通过 引用其他导航图
在导航图中,您可以使用 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)
}
使用 Bundle 传递参数
我们可以使用 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
支持以下顶部应用栏类型:
Toolbar
CollapsingToolbarLayout
ActionBar
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)
}