Navigation 的应用实践

在这里插入图片描述

1.基本使用

第一步,添加依赖

Navigation的依赖添加:

dependencies {
  def nav_version = "2.5.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"

  // 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:$nav_version"
}

为了方便在fragment之间传递参数,同时添加Safe Args的依赖,也可以选择不添加,使用Bundle进行fragment间的传递。

Safe Args依赖添加:

顶层 build.gradle 文件中包含以下 classpath

buildscript {
    repositories {
        google()
   }
    dependencies {
        def nav_version = "2.5.1"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
   }
}

添加插件,选择其一

plugins {
  //适用于 Java 模块或 Java 和 Kotlin 混合模块的 Java 语言代码
  id 'androidx.navigation.safeargs'
  //仅适用于 Kotlin 模块的 Kotlin 语言代码
  // id 'androidx.navigation.safeargs.kotlin'
}

第二步,创建导航宿主Activity以及需要的Fragment

导航宿主是 Navigation 组件的核心部分之一。导航宿主是一个空容器,用户在您的应用中导航时,目的地会在该容器中交换进出。

导航宿主必须派生于 NavHost。Navigation 组件的默认 NavHost 实现 (NavHostFragment) 负责处理 Fragment 目的地的交换。

注意:Navigation 组件旨在用于具有一个主 Activity 和多个 Fragment 目的地的应用。主 Activity 与导航图相关联,且包含一个负责根据需要交换目的地的 NavHostFragment。在具有多个 Activity 目的地的应用中,每个 Activity 均拥有其自己的导航图。

核心部分在于Activity的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <fragment
        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/nav_graph"
        />
</FrameLayout>

Activity中如无业务需求,那么一般无需在Activity添加对Navigation的操作。

注意点

android:name 属性包含 NavHost 实现的类名称。

app:navGraph 属性将 NavHostFragment 与导航图相关联。导航图会在此 NavHostFragment 中指定用户可以导航到的所有目的地。

app:defaultNavHost=“true” 属性确保您的 NavHostFragment 会拦截系统返回按钮。请注意,只能有一个默认 NavHost。如果同一布局(例如,双窗格布局)中有多个宿主,请务必仅指定一个默认 NavHost。

第三步,创建导航图

导航图是什么呢?在Navigation中,它是包含您的所有目的地和操作的一种资源文件。在Android studio可视化后,如下图所示:

在这里插入图片描述

其中,我们可以看到,里面由不同的界面和一系列的箭头连线组成。界面一般是Fragment,连线则是Fragment之间的跳转关系。其中有些连线还有一些向上的箭头,这个是表示在这次跳转中有fragment存在出栈的操作。

查看导航图的代码:

我们从中一步步分析往下。

首先,顶部的是navigation标签,代码如下:

<?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 > 元素是导航图的根元素。当您向图表添加目的地和连接操作时,可以看到相应的 和 元素在此处显示为子元素。如果您有嵌套图表,它们将显示为子 元素。

第四步,创建目的地

目的地,就是导航图中的一个个的界面,在Navigation中可以是Fragment,Activity,DialogFragment等…

这里我们主要介绍Fragment目的地。

在navigation标签下添加fragment标签,如下所示:

<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/navigation"
    app:startDestination="@id/AllInfoFragment">

    <fragment
        android:id="@+id/BaseInfoFragment"
        android:name="xxx.xxx.xxx.BaseInfoFragment"
        android:label="BaseInfoFragment"
        tools:layout="@layout/fragment_base_info">
    </fragment>
    
</navigation>
  • Label 字段包含该目的地的用户可读名称。例如,如果您使用 setupWithNavController() 将 NavGraph 连接到 Toolbar,就可能在界面上看到此字段。因此,我们建议您对此值使用资源字符串。
  • ID 字段包含该目的地的 ID,它用于在代码中引用该目的地。
  • Name 关联的类的名称。
  • Tools:layout可视化中显示的视图,对运行代码无影响
  • Navigation->startDestination 开始的目的地或者说该Navigation第一个页面

第五步,创建跳转

在navigation中跳转由实现,是每个目的地之间的逻辑连接,Action通常会将一个目的地连接到另一个目的地,当然也有全局Action,此类Action可让您从应用中的任意位置转到特定目的地。

这里我们主要介绍普通的Action。

<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/navigation"
    app:startDestination="@id/AllInfoFragment">

    <fragment
        android:id="@+id/BaseInfoFragment"
        android:name="xxx.xxx.xxx.BaseInfoFragment"
        android:label="BaseInfoFragment"
        tools:layout="@layout/fragment_base_info">
        <action
            android:id="@+id/action_BaseInfo_to_AllInfo"
            app:destination="@id/AllInfoFragment"
            app:popUpTo="@id/BaseInfoFragment"
            app:popUpToInclusive="true"
            app:launchSingleTop="true"/>
    </fragment>
    <fragment
        android:id="@+id/AllInfoFragment"
        android:name="xxx.xxx.xxx.AllInfoFragment"
        android:label="AllInfoFragment"
        tools:layout="@layout/fragment_all_info">

    </fragment>
</navigation>

Action会包含以下参数。

  • ID 字段包含该操作的 ID。
  • Destination 字段包含目的地 Fragment 或 Activity 的 ID。
  • popUpTo 表示跳转到某个tag,并将tag之上的元素出栈。
  • popUpToInclusive 为true表示会弹出tag,false则不会。
  • launchSingleTop 如果栈中已经包含了指定要跳转的界面,那么只会保留一个,不指定则栈中会出现两个界面相同的Fragment数据,可以理解为类似activity的 singleTop,即栈顶复用模式

详情:参考链接

调用:

val navHostFragment =
        supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
navController.navigate(R.id.action_BaseInfo_to_AllInfo)

//简化版本  
findNavController().navigate(R.id.action_BaseInfo_to_AllInfo)

第六步,传递参数

使用Safe Args进行传参,如果不使用的话,那么可以用Bundle代替。这里可以跳过。

示例代码:

<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/navigation"
    app:startDestination="@id/AllInfoFragment">

    <fragment
        android:id="@+id/BaseInfoFragment"
        android:name="xxx.xxx.xxx.BaseInfoFragment"
        android:label="BaseInfoFragment"
        tools:layout="@layout/fragment_base_info">
        <action
            android:id="@+id/action_BaseInfo_to_DynamicParameter"
            app:destination="@id/DynamicParameterFragment" />
    </fragment>
    
    <fragment
        android:id="@+id/DynamicParameterFragment"
        android:name="xxx.xxx.xxx.DynamicParameterFragment"
        android:label="DynamicParameterFragment"
        tools:layout="@layout/fragment_dynamic_parameter">

        <argument
            android:name="navigationParameterId"
            app:argType="integer"
            android:defaultValue="-1"
            />

        <argument
            android:name="navigationParameterName"
            app:argType="string"
            app:nullable="true"
            />

        <argument
            android:name="navigationFromAllInfo"
            app:argType="boolean"
            app:nullable="false"
            />
    </fragment>

</navigation>

简单的参数如上所示,由argument标签修饰,

  • name 参数名。
  • argType 参数类型。
  • nullable 可否为null。

支持的参数类型如下:

类型语法是否支持默认值是否由路由处理是否允许为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”
自定义Parcelableapp:argType=“”,其中 是 Parcelable 的完全限定类名称 支持默认值“@null”。不支持其他默认值。
自定义Serializableapp:argType=“”,其中 是 Serializable 的完全限定类名称 支持默认值“@null”。不支持其他默认值。
自定义Enumapp:argType=“”,其中 是 Enum 的完全限定名称是 - 默认值必须与非限定名称匹配(例如,“SUCCESS”匹配 MyEnum.SUCCESS)。

如果参数类型支持 null 值,您可以使用 android:defaultValue=“@null” 声明默认值 null。

替换操作中的目的地参数

所有导航至目的地的操作都使用目的地级的参数和默认值。如果需要,您可以通过在操作级定义参数来替换参数的默认值(如果不存在,则设置一个)。此参数必须与目的地中声明的参数具有相同的名称和类型。

以下 XML 展示了如何声明操作并替换上例中的目的地级参数:

<action android:id="@+id/startMyFragment"
    app:destination="@+id/myFragment">
    <argument
        android:name="myArg"
        app:argType="integer"
        android:defaultValue="1" />
</action>

启用 Safe Args 后,生成的代码会为每个操作包含以下类型安全的类和方法,以及每个发送和接收目的地。

为生成操作的每一个目的地创建一个类。该类的名称是在源目的地的名称后面加上“Directions”。例如,如果源目的地是名为 SpecifyAmountFragment 的 Fragment,则生成的类的名称为 SpecifyAmountFragmentDirections。

该类会为源目的地中定义的每个操作提供一个方法。

对于用于传递参数的每个操作,都会创建一个 inner 类,该类的名称根据操作的名称确定。例如,如果操作名称为 confirmationAction,,则类名称为 ConfirmationAction。如果您的操作包含不带 defaultValue 的参数,则您可以使用关联的 action 类来设置参数值。

为接收目的地创建一个类。该类的名称是在目的地的名称后面加上“Args”。例如,如果目的地 Fragment 的名称为 ConfirmationFragment,,则生成的类的名称为 ConfirmationFragmentArgs。可以使用该类的 fromBundle() 方法检索参数。

所以以上代码跳转到DynamicParameterFragment并附带参数的写法就如下所示:

BaseInfoFragmentDirections.actionBaseInfoToDynamicParameter(list[0].category_id,list[0].category_name,false)

官方链接:https://developer.android.google.cn/guide/navigation?hl=zh-cn

2.踩坑记录

第一点.navigation下的fragment声明周期

Navigation框架下的Fragment压栈时View会被销毁,再进入栈顶时会再次构建View。这就面临一个Activity不会存在的问题:如何恢复View的数据? 例如:常见的RecyclerView也会在入栈的时候被销毁,当用户返回时候,需要重新构建RecyclerView、绑定Adapter, 因此Adapter或者说是列表数据不能丢失,需要在ViewModel或者Fragment类成员变量中保存, 否则就只能再次请求网络加载数据(体验非常糟糕)。Fragment和View分家的问题,用下面的2张生命周期图可以阐释出来:

Navigation出现之前官方给出的Fragment生命周期如下图:(注意onDestroyView之处)

在这里插入图片描述
而LIfecycle,Navigation等组件出现之后,官方给出的Fragment生命周期图为下图:
在这里插入图片描述

Navigation框架下的Fragment生命周期分为 Fragment Lifecycle 和 View Lifecycle ,View Lifecycle被单独拎出来了,原因就在于Navigation框架下的非栈顶的Fragment均会被销毁View, 也即是 A跳转到B页面: A会执行onDestroyView销毁其 View (凡是和View相关的,如:Databinding、RecyclerView都会被销毁) , 但是Fragment本身会存在( Fragment本身的成员变量等 是不会被销毁的 ) 。为啥这样设计, 请参考Navigation的这个 Issue:Navigation, Saving fragment state , 很多人重写Navigation,使其能够保存View。这个例子请参考 起初Jetpack Navigation把我逼疯了,可是后来真香 ,大致的实现就是将官方的replace方式替换为Hide和Show。 但是不建议。

Navigation框架之下的正确状态流转应该是类似这的:

在这里插入图片描述

A 通过action打开B,A从 onResume转到onDestroyView,B从onAttach执行到onResume, 当B通过系统返回键返回到A时候,A从上图的onCreateView流转到onResume , 此过程中A的View经历销毁和重建,View(binding实例)的对象实例是不一样的,但是Fragment A这个实例始终相同。

这样的场景下,假设A存在一个网络新闻列表RecyclerView, RecyclerView随着View被销毁、重建。 如何保存其中的数据,避免每次返回到A的时候重新刷新数据(造成:上次浏览数据、位置丢失、额外的网络资源消耗), 因此RecyclerView中Adapter的数据项非常关键! 常见的保存方式有: 1、通过Fragment的成员变量 2、ViewModel 。方法2非常合适,在ViewModel的ViewModelScope通过协程请求网络数据,保存在ViewModel(ViewModel生命周期贯穿Fragment),可通过LiveData、普通变量保存数据,在 onViewCreated之后恢复数据。

参考链接:http://www.icodebang.com/article/274205

第二点.返回键监听

在业务处理中,经常在一些需要编辑的界面对返回键进行监听,比如当用户编辑了某一内容的时候,需要返回键进行拦截添加一些挽留的Dialog弹窗。

在fragment中我们可以使用如下方法实现。

//返回按钮拦截件监听
val callback: OnBackPressedCallback = object : OnBackPressedCallback(true){
    override fun handleOnBackPressed() {
        quitLogic()
   }
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner,callback)

第三点.Navigation中的返回栈

一开始由于对popUpTo参数的理解存在偏差,导致Navigation的返回栈不及预期。这里需要重点把控一些,Navigation在2.3.0版本后就没有了对返回栈清空的操作,所以在2.3.0后需要结合popUpTo与popUpToInclusive来实现清空栈的操作。

popUpTo表示会将返回栈中某个tag的以上的元素出栈。

popUpToInclusive为true表示会弹出tag,false则不会。

第四点.声明周期引发的一系列问题

参考:https://mp.weixin.qq.com/s/XdPYjC_6NP-0rmp8Y3hNpg

评论回复不及时,有问题的小伙伴可以点击链接:Android 学习交流 提问

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值