Navigation,是 Google 推出的应用内导航组件,也就是我们通常所说的屏幕切换。本文将从以下几个方面来介绍 Navigation。
应用内导航,也就是我们通常所说的屏幕切换,是 Android 开发中很关键的一部分,过去我们一般通过 Intent 或 Fragment 事务来实现应用内导航。它们的使用场景也都比较简单,比如说点击按键等等。
但是,如果想要完成更复杂一点的工作,又该怎么办呢?比如,在底部处理导航这种非常常见的导航模式时,不仅要确保用户点击底部导航栏后,应用界面可以正常跳转,而且还要突出显示正确的按钮。在处理返回栈的时候,也同样需要注意这个问题,以免给用户带来不必要的困扰。
新导航组件 Navigation 可以出色地处理这类复杂的场景,导航组件包括多个库、插件和工具。大大简化了 Android 应用内导航的开发工作。
总结起来 Navigation 具有以下的特点:
接下来看一下,通过“单个 Activity 嵌套多个 Fragment”的方法来完成导航工作。
在开始示例之前,先来介绍一下 Google 为什么推荐一个 App 只需要一个 Activity?
一个 App 只需要一个 Activity,说的是使用单个 Activity 配合多个 Fragment 的模式,我们来看看使用这种模式有什么好处:
既然使用单个 Activity 配合多个 Fragment 的模式有这么多的好处,接下来,我们总结一下如何正确的使用 Fragment。
通常创建 Fragment 有两种方式:
**1. 第一种方式就是通过将 fragment 放在 xml 布局文件中。**比如:
<fragment
android:id="@+id/fragment_about"
android:name="com.example.android.navigationadvancedsample.homescreen.About"
android:layout_width="match_parent"
android:layout_height="match_parent" />
通过这种方式创建的 Fragment 无法通过 FragmentTransition 的 remove() 方法移除,只能通过 View 的 visibility 的属性来控制。
2.第二种方式就是在代码中创建 Fragment 实例,通过 FragmentTransition 的 add() 方法,add() 方法的第一个参数是 Fragment 容器的id,通常是一个没有子View的 FrameLayout ,它决定了 Fragment 要在什么位置显示。
Fragment 本身是没有 onBackPressed() 方法的,所以没有直接的方法来监听设备的返回键,那如果我们想在 Fragment 中对返回键添加一些业务逻辑,而不是直接将 Fragment 弹出任务栈,该如何处理呢?
可以在 Fragment 中创建 OnBackPressedCallback 实例,通过实现它的 handleOnBackPressed() 方法来处理返回键的逻辑,最后通过 FragmentActivity 的 getOnBackPressedDispatcher().addCallback() 方法,将创建的 OnBackPressedCallback 实例添加到生命周期的监测中。
在 OnBackPressedDispatcher 源码的注释中,也给出了使用的示例。
/**
* Dispatcher that can be used to register {@link OnBackPressedCallback} instances for handling
* the {@link ComponentActivity#onBackPressed()} callback via composition.
*
* public class FormEntryFragment extends Fragment {
* {@literal @}Override
* public void onAttach({@literal @}NonNull Context context) {
* super.onAttach(context);
* OnBackPressedCallback callback = new OnBackPressedCallback(
* true // default to enabled
* ) {
* {@literal @}Override
* public void handleOnBackPressed() {
* showAreYouSureDialog();
* }
* };
* requireActivity().getOnBackPressedDispatcher().addCallback(
* this, // LifecycleOwner
* callback);
* }
* }
*
*/
public final class OnBackPressedDispatcher {
}
同样的,Fragment 本身也是没有 startFragmentForResult() 的,可以通过其他方法来实现同样的效果。
在 Fragment 中,通过 setTargetFragment() 方法设置目标 Fragment 和 requestCode。
public void setTargetFragment(@Nullable Fragment fragment, int requestCode) {
}
在跳转到目标 Fragment 后,可以在当前 Fragment 中通过下面这种方式,获取返回值。
targetFragment?.onActivityResult(targetRequestCode, Activity.RESULT_OK, Intent())
需要注意的是目标 Fragment 和被请求的 Fragment 必须在同一个 FragmentManager 的管理下,否则就会报错。
使用 Fragment 时,需要明确它各种生命周期回调函数。Fragment 不仅包含所有 Activity 的生命周期回调函数,同时结合自身特点,还有一些特有的回调函数,比如:onInflate()、onAttach()、onCreateView()、onViewCreated()、onActivityCreated()等等。先来看一张 Fragment 和 Activity 完整的生命周期流程图。
总结一下 Fragment 常用的回调函数:
接下来我们来看一下 Navigation 的组成,按照惯例,先给出一张类图:
使用 Navigation,需要在 AndroidStudio3.3及以上版本。 Navigation 一共由三个部分组成,它们相互配合工作,这三个部分分别是:Navigation Graph、NavHostFragment、NavController。
Navigation Graph 是一种新的资源类型,它是一个 xml 文件,用于集中保存所有与导航相关的信息。在 Android3.3 中提供的新导航编辑器能够可视化这些信息。可以把导航图当做一款用来创建导航图的图片编辑器。在导航图中,一个屏幕代表一个 Destination(目的地)也就是导航指向的下一个视图。
<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"
app:startDestination="@+id/home_dest">
<fragment
android:id="@+id/flow_step_one_dest"
android:name="com.example.android.codelabs.navigation.FlowStepFragment"
tools:layout="@layout/flow_step_one_fragment">
<argument
.../>
<action
android:id="@+id/next_action"
app:destination="@+id/flow_step_two_dest">
action>
fragment>
navigation>
总结一下 Navigation 的 xml 文件:
NavHostFragment,需要添加在布局文件中。以便进行后续的 Fragment 导航工作。NavHostFragment 相当于一个导航界面容器,用来换入和换出应用中各个 Destination 所代表的 Fragment。
我们来看一下 NavHostFragment 与 Activity 的关系:
按照 Google 推荐的一个 App 只需要一个 Activity 的思想,Activity 将包含顶部的工具栏,底部的导航栏,以及代表特定 Destination 的 Fragment。
按照这个思想,调整后的布局文件:
<LinearLayout
.../>
<androidx.appcompat.widget.Toolbar
.../>
<fragment
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/mobile_navigation"
app:defaultNavHost="true"
/>
<com.google.android.material.bottomnavigation.BottomNavigationView
.../>
LinearLayout>
总结一下调整后的布局文件:
NavController,需要在代码中为各个 NavHostFragment 添加对应的 NavController 以便管理和控制具体的导航行为。NavController 会根据导航图中的信息来执行相应的导航操作,并最终把需要显示的 Fragment 切换到 NavHostFragment 中。
介绍完 Navigation 的三个组成部分,我们再回过头来总结一下上面的类图:
Safe Args 是一个为 Navigation 组件服务的 gradle 插件,它会生成简单的对象和构建器类,以便对 Destination和 Action 进行类型安全的传递参数。
1. 首先,需要把这个 gradle 插件添加到代码中,在project 的 build.gradle 文件中:
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
//...
}
然后在 app/build.gradle 文件中:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'androidx.navigation.safeargs.kotlin'
android {
//...
}
2.在 Fragment 中通过属性,safe args会把所有带有参数的 Destionation 编译成为 XXXArgs 类,
<fragment
android:id="@+id/flow_step_one_dest"
android:name="com.example.android.codelabs.navigation.FlowStepFragment"
tools:layout="@layout/flow_step_one_fragment">
<argument
android:name="flowStepNumber"
app:argType="integer"
android:defaultValue="1"/>
<action...>
action>
fragment>
使用插件会对我们的原始导航语句进行处理,得到一个新生成的类,不用 xml id 来指代某个 Action,而是将 Action 关联到一个具体的 Destionation,可以为这个 Action 设置一个参数,如果传递的类型不正确,代码就会出现编译失败的情况。
传递参数的获取方法也很简单,只需要使用生成的 Args 类就可以了,这种方法允许仅对命名正确的参数进行类型安全访问。
val safeArgs: FlowStepFragmentArgs by navArgs()
val flowStepNumber = safeArgs.flowStepNumber
总结完 Navigation 的组成,接下来通过几个示例,介绍如何使用 Navigation。
利用项目中的 Fragment 来创建 Destination,导航图上的箭头叫做 Action,代表可以在应用中使用的几条导航路径。选中其中一个 Action 后,我们就能看到一组内嵌信息,包括各个 Destionation 间传递的信息、转场动画、返回栈操作等。选中一个 Destination 之后,还可以定义深层链接 url 和启动选项等内容。它们都属于导航图中 xml 的一部分。
上面介绍的都是 Navigation 的一些简单的应用,下面来介绍通过 Navigation 实现底部导航的方法。
Navigation 包含了一个针对 java语言的导航 UI 库,以及一些 Kotlin ktx 扩展函数,这些库和函数用于支持如选项菜单、底部导航、导航视图、导航抽屉的导航,也可以与 ActionBar、工具栏、可折叠式工具栏搭配使用。
在实现底部导航的过程中,首先需要将底部导航添加到 xml 中,接着再创建一个菜单 xml。
<LinearLayout>
<fragment
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="@navigation/mobile_navigation"
.../>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_nav_menu" />
LinearLayout>
在这个步骤中,需要确保菜单的 xml id 和导航图中的底部导航指向的 Destination 的 xml id 相匹配。
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@id/home_dest"
android:icon="@drawable/ic_home"
android:title="@string/home" />
<item
android:id="@id/deeplink_dest"
android:icon="@drawable/ic_android"
android:title="@string/deeplink" />
menu>
然后,利用导航 UI 来处理剩下的工作。
private fun setupBottomNavMenu(navController: NavController) {
val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav_view)
bottomNav?.setupWithNavController(navController)
}
通过上面这行代码,具体的导航工作便托管给了 NavController。
示例代码地址 https://github.com/sguotao/google_jetpack_example
还可以使用 Navigation 来处理 DeepLink,包括 widgets, notifications, 和 web links。Navigation 提供了一个NavDeepLinkBuilder 类来构造一个 PendingIntent,它将把用户带到指定的 Destination。
这里通过一个 Widget 示例,简单介绍如何通过 Navigation 实现 DeepLink。
首先,创建一个 AppWidgetProvider 的子类,重写 onUpdate() 方法,通过 NavDeepLinkBuilder 构建一个 PendingIntent 。
class DeepLinkAppWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
val remoteViews = RemoteViews(
context.packageName,
R.layout.deep_link_appwidget
)
val args = Bundle()
args.putString("myarg", "From Widget")
val pendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.mobile_navigation)
.setDestination(R.id.deeplink_dest)
.setArguments(args)
.setComponentName(NavigationActivity::class.java)
.createPendingIntent()
remoteViews.setOnClickPendingIntent(R.id.deep_link_button, pendingIntent)
appWidgetManager.updateAppWidget(appWidgetIds, remoteViews)
}
}
然后在 AndroidManifest.xml 中进行注册
<receiver android:name=".DeepLinkAppWidgetProvider">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/deep_link_appwidget_info" />
receiver>
示例代码地址 https://github.com/sguotao/google_jetpack_example
Navigation 作为应用内导航组件,它提供了应用内导航的一副地图,地图中的各个目的地之间可以进行类型安全的传参,设置转场动画等。
Navigation 包含三个组成部分,Navigation Graph 、NavHostFragment 和 NavController。Navigation Graph是一种新的资源类型,NavHostFragment 相当于一个导航界面容器,具体的导航行为都是通过 NavController 完成的。
更多内容,可以订阅 我的博客
Navigation
Jetpack Navigation
android-lifecycle