踩坑!Android Jetpack组件间库之Navigation

  • 根据迁移到 AndroidX 文档,您的 gradle.properties 文件中还必须具有 android.useAndroidX=true

  • 官方对Navigation的设计是不会对Fragment进行状态保存,需要开发者在ViewModel实现,所以无论是跳转到下一页面或者回退到上一页面,Fragment都会重新走生命周期方法,对此,本文也给出了解决方案,参考提供安全可靠的 Navigation 操作

基本使用

在Project的 build.gradle 文件添加以下依赖:

buildscript {

repositories {

google()

}

dependencies {

def nav_version = “2.3.5”

classpath “androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5”

}

}

在app的 build.gradle 文件添加以下依赖:

apply plugin: “androidx.navigation.safeargs”

dependencies {

// Kotlin相关依赖

implementation “androidx.navigation:navigation-fragment-ktx:2.3.5”

implementation “androidx.navigation:navigation-ui-ktx:2.3.5”

}

创建导航图:

  1. 在“Project”窗口中,右键点击 res 目录,然后依次选择 New > Android Resource File。此时系统会显示 New Resource File 对话框。

  2. File name 字段中输入名称,例如“nav_graph”。

  3. Resource type 下拉列表中选择 Navigation,然后点击 OK

当添加首个导航图时,Android Studio 会在 res 目录内创建一个 navigation 资源目录。该目录包含您的导航图资源文件(例如 nav_graph.xml)。

nav_graph1

添加nav_graph.xml后,Android Studio 会在 Navigation Editor 中打开该文件。在 Navigation Editor 中,您可以直观地修改导航图,或直接修改底层 XML。

下一步我们需要点击+号,去创建一个destination,点击Create new destination创建 HomeFragment

nav_graph2

destination是目的地,即你想要去的地方。可以是Fragment或者Activity,但最常见的是Fragment,Navigation组件的目的就是方便开发者在一个Activity中管理多个Fragment。

这时候就会生成一个Fragment,右上角默认为startDestinationNavHostFragment容器首先要展示的Fragment。如果发现页面不能预览,可以手动编辑XML文件在fragment节点添加页面对应的布局引用就可以了(例如 tools:layout="@layout/fragment_home")。

nav_graph3

点击右上角Split可以看到XML文件。这里的<navigation> 元素是导航图的根元素。当我们向导航图添加目的地和连接操作时,可以看到相应的 <destination><action> 元素在此处显示为子元素。如果您有嵌套视图,它们将显示为子 <navigation> 元素。

nav_graph4

向 Activity 添加 NavHost

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

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

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

接下来我们在MainAcitivty的XML文件添加 NavHostFragment,这里推荐使用的是androidx.fragment.app.FragmentContainerView而非fragment

<androidx.fragment.app.FragmentContainerView

android:id=“@+id/nav_host_fragment”

android:name=“androidx.navigation.fragment.NavHostFragment”

android:layout_width=“0dp”

android:layout_height=“0dp”

app:layout_constraintLeft_toLeftOf=“parent”

app:layout_constraintRight_toRightOf=“parent”

app:layout_constraintTop_toTopOf=“parent”

app:layout_constraintBottom_toBottomOf=“parent”

app:defaultNavHost=“true”

app:navGraph=“@navigation/nav_graph” />

  • android:name 属性指向 NavHost 实现的类名称。

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

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

向导航图添加目的地

我们可以从现有的 Fragment 或 Activity 创建目的地。还可以使用 Navigation Editor 创建新目的地,或创建占位符以便稍后替换为 Fragment 或 Activity。

连接目的地

可以使用 Navigation Editor 将两个目的地连接起来,具体操作步骤如下:

  • 在 Design 标签页中,将鼠标悬停在目的地的右侧,该目的地为您希望用户从中导航出来的目的地。该目的地右侧上方会显示一个圆圈;

  • 点击希望用户导航到的目的地,并将光标拖动到该目的地的上方,然后松开。这两个目的地之间生成的线条表示操作。

点击右上角Code切换到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/homeFragment”>

<fragment

android:id=“@+id/homeFragment”

android:name=“com.soushin.tinmvvm.mvvm.ui.fragment.HomeFragment”

android:label=“HomeFragment”

tools:layout=“@layout/fragment_home”>

<action

android:id=“@+id/action_homeFragment_to_categoryFragment”

app:destination=“@id/categoryFragment” />

<fragment

android:id=“@+id/categoryFragment”

android:name=“com.soushin.tinmvvm.mvvm.ui.fragment.CategoryFragment”

android:label=“CategoryFragment”

tools:layout=“@layout/fragment_category”/>

在导航图中,连接由 <action> 元素表示。连接至少应包含<action>的 ID 和用户应转到的目的地的 ID(destination指向的id)。

导航到目的地

导航到目的地是使用 NavController 完成的,它是一个在 NavHost 中管理应用导航的对象。每个 NavHost 均有自己的 NavController。您可以使用以下方法之一获取 NavController

Kotlin

Java

在目的地之间传递数据

启用 Safe Args 后,会为每个action的起点和终点生成类型安全的类和方法。具体操作如下:

  1. Navigation Editor 中,点击接收参数的终点;

  2. Attributes 面板中,点击 Add ( + );

  3. 在显示的 Add Argument Link 窗口中,输入参数名称、参数类型、参数是否可为 null,以及默认值(如果需要);

  4. 点击 Add。请注意,该参数现在会显示在 Attributes 面板的 Arguments 列表中;

  5. 您还可以看到该参数已添加到 XML 中。点击 Code 标签页以切换到 XML 视图,就会发现您的参数已添加到接收该参数的目的地。相关示例如下所示:

<fragment

android:id=“@+id/categoryFragment”

android:name=“com.soushin.tinmvvm.mvvm.ui.fragment.CategoryFragment”

android:label=“CategoryFragment”

tools:layout=“@layout/fragment_category”>

<argument

android:name=“pageType”

app:argType=“integer” />

除了可以在XML文件中静态传值,还可以动态设置。我们在起点通过NavControllernavigate()导航,并且使用HomeFragmentDirections.actionHomeFragmentToCategoryFragment(99)进行传参,然后在终点通过CategoryFragmentArgs获取传过来的参数:

//起点设置参数 类型安全

Navigation.findNavController(v).navigate(HomeFragmentDirections.actionHomeFragmentToCategoryFragment(99))

//终点接收参数 类型安全

val args : CategoryFragmentArgs by navArgs()

println(args.pageType)

踩坑:当我们快速点击按钮会重复调用navigate()导致异常如下:

java.lang.IllegalArgumentException: Navigation action/destination com.soushin.tinmvvm:id/action_homeFragment_to_categoryFragment cannot be found from the current destination Destination(com.soushin.tinmvvm:id/categoryFragment) label=CategoryFragment class=com.soushin.tinmvvm.mvvm.ui.fragment.CategoryFragment

这是因为第一次调用navigate()后当前页面已经换了,而我们的action是明确了起点和终点的,所以当第二次调用的时候起点不再是HomeFragment就会报上述异常,这在页面跳转时添加转场动画会显得非常明显(转场动画有duration),只需要在跳转前判断currentDestination是否是当前页面的destination即可。示例如下:

if (Navigation.findNavController(v).currentDestination?.id != R.id.homeFragment) return

使用NavigationUI更新页面

Navigation 组件包含 NavigationUI 类。此类包含多种静态方法,可帮助您使用ToolbarNavigationViewBottomNavigationViewCollapsingToolbarLayoutDrawerLayout来管理导航。

顶部标题栏(Toolbar/ActionBar/CollapsingToolbarLayout)

nav_graph5

利用 NavigationUI 内置的方法,我们可以在用户浏览应用的过程中自动更新顶部标题栏中的内容。例如,NavigationUI 可使用导航图中的目的地标签及时更新顶部标题栏的标题。

<fragment …

android:label=“Page title”>

还有个语法糖就是,如果我们按下面介绍的顶部标题栏实现方法使用 NavigationUI,可以在标签中使用 {argName} 格式,从而根据提供给相应目的地的参数来自动填充附加到目的地的标签。

NavigationUI 支持以下顶部标题栏类型:

AppBarConfiguration

NavigationUI 使用 AppBarConfiguration 对象管理在应用显示区域左上角的导航按钮的行为。导航按钮的行为会根据当前页面是否位于顶层目的地而变化。

顶层目的地是一组存在层次关系的目的地中的根级或最高级目的地。顶层目的地不会在顶部标题栏中显示“返回”按钮,因为不存在更高等级的目的地。默认情况下,应用的目的地是唯一的顶层目的地。

当用户位于顶层目的地时,如果目的地使用了 DrawerLayout,导航按钮会变为抽屉式导航栏图标。如果目的地没有使用 DrawerLayout,导航按钮处于隐藏状态。当用户位于任何其他目的地时,导航按钮会显示为返回按钮。在配置导航按钮时,如需将起始目的地用作唯一顶层目的地,请创建 AppBarConfiguration 对象并传入相应的导航图,如下所示:

val appBarConfiguration = AppBarConfiguration(navController.graph)

在更多情况下,我们可能需要定义多个顶层目的地,而不是使用默认的目的地。这种情况的一种常见用例是 BottomNavigationView,在此场景中,同级屏幕可能彼此之间并不存在层次关系,并且可能各自有一组相关的目的地。对于这样的情况,您可以改为将一组目的地 ID 传递给构造函数,如下所示:

val appBarConfiguration = AppBarConfiguration(setOf(R.id.main, R.id.profile))

创建Toolbar

如需使用 NavigationUI 创建工具栏,请先在主 activity 中定义该工具栏,如下所示:

<androidx.appcompat.widget.Toolbar

android:id=“@+id/toolbar” />

<androidx.fragment.app.FragmentContainerView

android:id=“@+id/nav_host_fragment”

… />

接下来,从主 activity 的 onCreate() 方法中调用 setupWithNavController()"),如下所示:

override fun onCreate(savedInstanceState: Bundle?) {

setContentView(R.layout.activity_main)

val navController = findNavController(R.id.nav_host_fragment)

val appBarConfiguration = AppBarConfiguration(navController.graph)

findViewById(R.id.toolbar)

.setupWithNavController(navController, appBarConfiguration)

}

注意:使用 Toolbar 时,Navigation 组件会自动处理导航按钮的点击事件,因此我们不需要重写 onSupportNavigateUp()")。

如需将导航按钮配置为在所有目的地都显示为返回按钮,那么在构建 AppBarConfiguration 时,请为顶层目的地传递一组空白目的地 ID。例如,如果我们有第二个 activity,其应在所有目的地的 Toolbar 中显示返回按钮,这样做可能就很有用。这样一来,当返回堆栈上没有其他目的地时,用户便可导航回父 activity。您可以使用 setFallbackOnNavigateUpListener()") 控制在 navigateUp() 不另行执行任何操作时的回退行为,如以下示例所示:

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(R.id.toolbar)

.setupWithNavController(navController, appBarConfiguration)

}

使用CollapsingToolbarLayout

如需在工具栏中添加 CollapsingToolbarLayout,请先在 activity 中定义工具栏和周围布局,如下所示:

<com.google.android.material.appbar.AppBarLayout

android:layout_width=“match_parent”

android:layout_height=“@dimen/tall_toolbar_height”>

<com.google.android.material.appbar.CollapsingToolbarLayout

android:id=“@+id/collapsing_toolbar_layout”

android:layout_width=“match_parent”

android:layout_height=“match_parent”

app:contentScrim=“?attr/colorPrimary”

app:expandedTitleGravity=“top”

app:layout_scrollFlags=“scroll|exitUntilCollapsed|snap”>

<androidx.appcompat.widget.Toolbar

android:id=“@+id/toolbar”

android:layout_width=“match_parent”

android:layout_height=“?attr/actionBarSize”

app:layout_collapseMode=“pin”/>

</com.google.android.material.appbar.CollapsingToolbarLayout>

</com.google.android.material.appbar.AppBarLayout>

<androidx.fragment.app.FragmentContainerView

android:id=“@+id/nav_host_fragment”

… />

接着,通过主 activity 的 onCreate 方法调用 setupWithNavController()"),如下所示:

override fun onCreate(savedInstanceState: Bundle?) {

setContentView(R.layout.activity_main)

val layout = findViewById(R.id.collapsing_toolbar_layout)

val toolbar = findViewById(R.id.toolbar)

val navHostFragment =

supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment

val navController = navHostFragment.navController

val appBarConfiguration = AppBarConfiguration(navController.graph)

layout.setupWithNavController(toolbar, navController, appBarConfiguration)

}

使用ActionBar

如需向默认操作栏添加导航支持,请通过主 activity 的 onCreate() 方法调用 setupActionBarWithNavController()"),如下所示。请注意,您需要在 onCreate() 之外声明 AppBarConfiguration,因为您在替换 onSupportNavigateUp() 时也使用该方法:

private lateinit var appBarConfiguration: AppBarConfiguration

override fun onCreate(savedInstanceState: Bundle?) {

val navHostFragment =

supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment

val navController = navHostFragment.navController

appBarConfiguration = AppBarConfiguration(navController.graph)

setupActionBarWithNavController(navController, appBarConfiguration)

}

接着,替换 onSupportNavigateUp() 以处理向上导航:

override fun onSupportNavigateUp(): Boolean {

val navController = findNavController(R.id.nav_host_fragment)

return navController.navigateUp(appBarConfiguration)

|| super.onSupportNavigateUp()

最后

在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
figuration(navController.graph)

setupActionBarWithNavController(navController, appBarConfiguration)

}

接着,替换 onSupportNavigateUp() 以处理向上导航:

override fun onSupportNavigateUp(): Boolean {

val navController = findNavController(R.id.nav_host_fragment)

return navController.navigateUp(appBarConfiguration)

|| super.onSupportNavigateUp()

最后

在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-S3HOZ9HA-1715413734212)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值