继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Android 架构组件之 Navigation

GT9456
关注TA
已关注
手记 89
粉丝 11
获赞 16

Navigation,是 Google 推出的应用内导航组件,也就是我们通常所说的屏幕切换。本文将从以下几个方面来介绍 Navigation。

  • 为什么要使用 Navigation ?
  • Google 为什么推荐一个 App 只需要一个 Activity?
  • 分析 Navigation 的组成及原理
  • 通过示例,介绍如何使用 Navigation
  • 总结一下 Navigation 的使用

1. 为什么要使用 Navigation ?

应用内导航,也就是我们通常所说的屏幕切换,是 Android 开发中很关键的一部分,过去我们一般通过 Intent 或 Fragment 事务来实现应用内导航。它们的使用场景也都比较简单,比如说点击按键等等。

但是,如果想要完成更复杂一点的工作,又该怎么办呢?比如,在底部处理导航这种非常常见的导航模式时,不仅要确保用户点击底部导航栏后,应用界面可以正常跳转,而且还要突出显示正确的按钮。在处理返回栈的时候,也同样需要注意这个问题,以免给用户带来不必要的困扰。

新导航组件 Navigation 可以出色地处理这类复杂的场景,导航组件包括多个库、插件和工具。大大简化了 Android 应用内导航的开发工作。

总结起来 Navigation 具有以下的特点:

  • 简化常见导航模式(比如底部导航)的设置工作;
  • 组件还可以管理返回栈;
  • 自动处理 fragment 事务;
  • 简化类型安全参数传递;
  • 管理转场动画;
  • 轻松设置深度链接 DeepLink;
  • 组件还会把收集到的全部导航信息统一存储在应用内的导航图里,让这些信息以一种可视化的方式呈现出来;
  • 导航组件默认支持 Fragment 和 Activity,也可以通过扩展库来支持其它屏幕实现,比如自定义视图。

接下来看一下,通过“单个 Activity 嵌套多个 Fragment”的方法来完成导航工作。

2. Google 为什么推荐一个 App 只需要一个 Activity?

在开始示例之前,先来介绍一下 Google 为什么推荐一个 App 只需要一个 Activity?

一个 App 只需要一个 Activity,说的是使用单个 Activity 配合多个 Fragment 的模式,我们来看看使用这种模式有什么好处:

  • 我们都知道 Activity 是 Android 的四大组件之一,本身受到 AMS 的管理。在 MVP 和 MVVM 模式以前,Activity 通常会耦合 View 层和 Model 层的业务逻辑,这使得 Activity 非常的庞大,使用的系统资源也比 Fragment 要多。所以使用这种模式,会比多个 Activity 的模式更节省资源,更加的流畅。
  • 将 UI 拆分成多个 Fragment 的形式,更便于复用,这是 Activity 无法比拟的。
  • 最后,使用 Fragment 页面之间传递数据更灵活。在 Activity 之前传递数据,需要对象序列化。这是由于 Activity 受到 AMS 的管理,而 AMS 属于系统进程,跨进程的数据传递,都需要序列化。而 Fragment 就没有这样的限制,灵活性更高。

既然使用单个 Activity 配合多个 Fragment 的模式有这么多的好处,接下来,我们总结一下如何正确的使用 Fragment。

2.1 如何创建 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 要在什么位置显示。

2.2 Fragment 中如何处理 onBackPressed ?

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.
 * <pre>
 * 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);
 *     }
 * }
 * </pre>
 */
public final class OnBackPressedDispatcher {
}

2.3 Fragment 中如何处理 startFragmentForResult ?

同样的,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 的管理下,否则就会报错。

2.4 梳理 Fragment 的生命周期回调函数

使用 Fragment 时,需要明确它各种生命周期回调函数。Fragment 不仅包含所有 Activity 的生命周期回调函数,同时结合自身特点,还有一些特有的回调函数,比如:onInflate()、onAttach()、onCreateView()、onViewCreated()、onActivityCreated()等等。先来看一张 Fragment 和 Activity 完整的生命周期流程图。

complete_android_fragment_lifecycle.png_
图片来源: https://github.com/xxv/android-lifecycle

总结一下 Fragment 常用的回调函数:

  • onInflate(),如果 Fragment 是通过将 fragment 放在 xml 布局文件的方式创建时,会优先 onAttach() 方法,先回调 onInflate() 方法。
  • onAttach(),当 Fragment 与 Activity 已经完成绑定后,会回调 onAttach() 方法,方法接收的 Context 实例就是当前 Fragment 所依附的 Activity。重写 onAttach() 方法时要调用 super.onAttach() 方法,否则 getActivity() 方法会返回 null。
  • onCreate(),在 onAttach() 执行完后,会回调 onCreate() 方法,可以通过 savedInstanceState 获取之前保存的一些状态值。
  • onCreateView(),在 onCreate() 执行完后,会回调 onCreateView() 方法,返回一个View用来初始化Fragment 的布局。如果返回的 view 是 null,那么 Fragment 的 onViewCreated() 将会被跳过。如果是在 xml中定义 fragment 标签并用 name 指定 Fragment ,则 onCreateView() 方法不允许返回 null ,否则就会报错。
  • onActivityCreated(),回调 onActivityCreated() 方法时,与 Fragment 绑定的 Activity 的 onCreate() 方法已经执行完成并返回。
  • onStart(),回调 onStart() 方法时,Fragment 所在的 Activity 由不可见变为可见状态。
  • onResume(),回调 onResume() 方法时,Fragment 所在的 Activity 处于活动状态,用户可与之交互。
  • onPause(),回调 onPause() 方法时,Fragment 所在的 Activity 处于暂停状态,但依然可见,用户不能与之交互。
  • onStop(),回调 onStop() 方法时,Fragment 所在的 Activity 完全不可见。
  • onSaveInstanceState(),保存当前 Fragment 的状态。
  • onDestroyView(),销毁与 Fragment 有关的视图,但未与 Activity 解除绑定,一般在这个回调里解除Fragment 对视图的引用。
  • onDestroy(),销毁 Fragment,通常按 Back 键退出或者 Fragment 被移除 FragmentManager 时,回调 onDestroy() 方法。通常在 onDestroy() 方法中,完成一些清理的操作。
  • onDetach(),解除与 Activity 的绑定。在 onDestroy() 方法之后调用。如果调用 super.onDetach() , getActivity() 方法将返回 null。

3. 分析 Navigation 的组成及原理

接下来我们来看一下 Navigation 的组成,按照惯例,先给出一张类图:

Navigation_CD.jpg_watermarklogo

使用 Navigation,需要在 AndroidStudio3.3及以上版本。 Navigation 一共由三个部分组成,它们相互配合工作,这三个部分分别是:Navigation Graph、NavHostFragment、NavController。

3.1 Navigation Graph

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 文件:

  • 是 Navigation Graph xml 文件的根节点;
  • 包含一个或多个以 或 元素表示的 Destination;
  • 的 app:startDestination 属性,它指定用户首次打开应用程序时默认启动的 Destination;

3.2 NavHostFragment

NavHostFragment,需要添加在布局文件中。以便进行后续的 Fragment 导航工作。NavHostFragment 相当于一个导航界面容器,用来换入和换出应用中各个 Destination 所代表的 Fragment。

我们来看一下 NavHostFragment 与 Activity 的关系:

navigationhostfragment_activity.png_wate

按照 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>

总结一下调整后的布局文件:

  • 这个布局文件是 Activity 对应的,它包含顶部工具栏,底部导航栏,和一个 Fragment;
  • Fragment 的 name 是 androidx.navigation.fragment.NavHostFragment,其中 app:defaultNavHost=“true” 属性是将系统后退按钮连接到 NavHostFragment,app:navGraph 属性将 NavHostFragment 与 Navigation Graph 联系起来。

3.3 NavController

NavController,需要在代码中为各个 NavHostFragment 添加对应的 NavController 以便管理和控制具体的导航行为。NavController 会根据导航图中的信息来执行相应的导航操作,并最终把需要显示的 Fragment 切换到 NavHostFragment 中。

介绍完 Navigation 的三个组成部分,我们再回过头来总结一下上面的类图:

  • NavigationHostFragment 是一个 Fragment,同时实现了 NavHost 接口,NavHostFragment 相当于一个导航界面容器,用来换入和换出各个 Fragment;
  • NavController 管理和控制具体的导航行为,可以通过NavigationHostFragment 的 findNavController() 方法获取 NavController 实例;
  • NavHostController 是 NavController 的子类,可以通过配置 NavOptions 来设置 Fragment 的转场动画;通过 NavDeepLinkBuilder 来实现 DeepLink 的导航;
  • NavigationUI 类提供了多个静态方法,navigation-ui-ktx 提供了多个 Kotlin 扩展函数,通过这些方法和函数,可以使用菜单,抽屉和底部导航进行导航。

3.4 Safe Args 插件

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

4. 通过示例,介绍如何使用 Navigation

总结完 Navigation 的组成,接下来通过几个示例,介绍如何使用 Navigation。

利用项目中的 Fragment 来创建 Destination,导航图上的箭头叫做 Action,代表可以在应用中使用的几条导航路径。选中其中一个 Action 后,我们就能看到一组内嵌信息,包括各个 Destionation 间传递的信息、转场动画、返回栈操作等。选中一个 Destination 之后,还可以定义深层链接 url 和启动选项等内容。它们都属于导航图中 xml 的一部分。

4.1 Navigation 实现底部导航

上面介绍的都是 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。

4.2 Navigation 实现 DeepLink

还可以使用 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>

5. 总结一下 Navigation 的使用

Navigation 作为应用内导航组件,它提供了应用内导航的一副地图,地图中的各个目的地之间可以进行类型安全的传参,设置转场动画等。

Navigation 包含三个组成部分,Navigation Graph 、NavHostFragment 和 NavController。Navigation Graph是一种新的资源类型,NavHostFragment 相当于一个导航界面容器,具体的导航行为都是通过 NavController 完成的。

更多内容,可以订阅 我的博客


参考文献

打开App,阅读手记
1人推荐
发表评论
随时随地看视频慕课网APP