这是一个新的系列文章,我们称之为 "Modern Android Development 技巧",简称为 "MAD Skills"。本系列文章致力于帮助开发者们打造更好的现代 Android 开发体验,敬请关注。
今天为大家发布本系列文章中的第三篇: 在应用中导航时使用 SafeArgs。如果您想回顾过去发布的内容,请参考下面链接查看:
这篇文章主要介绍 SafeArgs,它属于导航组件,并且可以在应用不同的目的地 (界面) 之间提供更加便捷的数据传递功能。
简介
当您在应用中导航到不同目的地的时候,可能会需要传递数据。为了避免使用全局对象引用,通过数据传递可以实现更好的代码封装结构,这样不同的 fragment 或者 activity 仅需要分享它们所需的数据即可。
导航组件可以通过 Bundles 传递数据,这个机制也可用于 Android 中跨 activity 传递数据。
这里我们也可以使用同样的方式,为要传递的数据创建一个 Bundle,然后在接收侧将数据提取出来。
不过导航组件有更好的方法: SafeArgs。
SafeArgs 是一个 gradle 插件,它可以帮助您在 导航图 中输入需要传递的数据信息。然后它会生成代码帮您解决创建 Bundle 时所需完成的冗长的过程,并且在接收侧提取数据。
您也可以直接使用 Bundle,但是我们建议使用 SafeArgs。不仅仅是为了代码更简洁,更多的是它为数据增加了类型安全的保障,使得代码具备更好的健壮性。
为了向大家展示 SafeArgs 的效果,我将继续使用之前在 Dialog Destinations 演示过的 Donut Tracker (甜甜圈追踪) 应用。如果您希望随着文章的讲解进行同步操作,请下载 应用源码,并在 Android Studio 中打开。
制作甜甜圈的时候到了
我们的 donut tracking 应用又来了:
Donut Track: 就是这个 App,它又来了
Donut Tracker 会显示甜甜圈的列表,每个列表项含有名称、描述和评分信息,这些内容有些是我添加的,有些是通过在点击 悬浮操作按钮 (FAB) 弹出的对话框中填写的。
点击悬浮操作按钮会弹出对话框填写新的甜甜圈信息
仅仅可以添加新的甜甜圈的信息是不够的,我还希望可以修改已有甜甜圈的信息。没准我拿到一张甜甜圈的照片,或者我希望提升之前的评分。
比较自然的实现方法是点击列表项,然后打开之前添加甜甜圈时的对话框,然后我可以在这里修改甜甜圈的信息。但是应用如何知道对话框里显示哪个甜甜圈的信息呢?代码里需要传递所点击的列表项的信息。在这里,它需要将对应表项的 id 从列表所在的 fragment 传递到对话框所在的 fragment,然后对话框可以根据 id 从数据库里找到对应甜甜圈的信息,并且填充到表单里。
要传递 id,这里我们使用 SafeArgs
来实现。
使用 SafeArgs
这里我需要说明一下,我已经完成了全部的代码,大家可以在 GitHub 的 示例 中找到完整的代码。所以接下来我会给大家讲解每个步骤,并且让大家看到示例代码的效果,而不是简简单单带着大家完成代码。
首先,我需要添加一些依赖库。
SafeArgs 和导航组件的其它模块不太一样,它本身并不是一个 API,而是一个可以生成代码的 gradle 插件。所以需要将它设置为 gradle 依赖,并且在构建时使其能够正确运行来生成所需的代码。
首先我在项目级的 build.gradle 文件的依赖部分中添加了下面的内容:
def nav_version = "2.3.0"
// 获取最新的版本号 https://developer.android.google.cn/jetpack/androidx/releases/navigation
classpath “androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version”
这里用到了 2.3.0 版本。如果您看到这篇文章的时候较晚,那么应该会有一个更新的版本供您使用。只要和您所使用的导航组件 API 的其它模块的版本一致就可以了。
然后我添加了下面的内容到 app 模块的 build.gradle 文件中。它使得在调用 SafeArgs 的时候可以生成所需的代码。
apply plugin: "androidx.navigation.safeargs.kotlin"
这里,gradle 提示需要同步,所以我点击一下 "Sync Now"。
这是一个您不应该忽略的提示
接下来,在导航图中创建并传递所需的数据。
需要数据的目标界面是对话框 donutEntryDialogFragment
,它需要知道所需显示的对象的信息。点击目标界面会在右侧显示相关属性。
点击目标界面会显示该界面的属性列表,您可以在这里输入需要传递的数据
在 Arguments 窗格点击 + 可以添加数据,会弹出下面所示的对话框。这里我希望传递的是所需显示的甜甜圈信息,所以数据类型设置为 Long,和数据库里的 id 的数据类型一致。
添加数据的时候会显示这个对话框,这里可以输入数据类型、默认值和其它所需的信息
需要注意的是当我定义数据类型为 Long 的时候,Nullable 的位置会变成灰色。这是因为 Java 编程语言中,基础数据类型 (Integer
、Boolean
、Float
、Long
) 是基于原始数据类型 (int
、bool
、float
、long
) 进行封装的,而原始数据类型不可为空,所以我们在使用基础数据类型的时候需要保证数据非空。
另外需要注意的是,应用现在使用该对话框添加新的元素 (我在上一篇文章 使用导航组件: 对话框目的地 | MAD Skills 中已经介绍),同时也使用该对话框编辑已有元素。所以并不一定会传递元素 id,当用户创建新元素的时候,代码应该能够判断当前并无元素信息需要显示。所以我在对话框中 Default Value (默认值) 的位置输入了 -1,因为 -1 并不是一个有效的索引值。当代码导航至该界面并且没有数据传递的时候,-1 就会作为默认值传递,接收端的代码需要使用该值判断用户现在需要创建一个新的甜甜圈。
到这里,我们执行 build 操作,gradle 就会针对所输入的数据生成相应的代码。这一点很重要,因为不是这样的话,Android Studio 就无法知道想要调用的函数在自动生成代码中的位置。
您可以在项目结构树的 "java(generated)" 分支下找到上面过程中生成的代码的执行结果。在子目录中,可以看到有新文件生成,它们负责传递和获取数据。
在 DonutListDirections
中,您可以找到 companion
对象,它是用于导航至对话框的 API。
companion object {
fun actionDonutListToDonutEntryDialogFragment(
itemId: Long = -1L): NavDirections =
ActionDonutListToDonutEntryDialogFragment(itemId)
}
这里 navigate()
并没有使用最初的 Action
,而是使用了 NavDirections
对象。它既封装了 action (我们可以通过 action 导航至对话框),同时还封装了早期创建的变量。
需要注意的是上面的 actionDonutListToDonutEntryDialogFragment()
函数需要一个 Long 类型的参数,我们之前创建了相关变量,并且给它赋值为 -1。所以如果我们在调用该函数的时候不加参数,该方法会返回一个 NavDirections
对象,并且它的 itemId 为 -1。
在另一个生成的文件 DonutEntryDialogFragmentArgs
中,您可以看到 fromBundle()
函数包含从目标对话框获取数据的代码:
fun fromBundle(bundle: Bundle): DonutEntryDialogFragmentArgs {
// ...
return DonutEntryDialogFragmentArgs(__itemId)
}
现在我可以利用生成的代码成功传递和获取数据了。首先,我在 DonutEntryDialogFragment
类中编写代码来获取 itemId
数据,并且确定用户的意图是添加一个新的甜甜圈还是编辑一个已有的甜甜圈:
val args: DonutEntryDialogFragmentArgs by navArgs()
val editingState =
if (args.itemId > 0) EditingState.EXISTING_DONUT
else EditingState.NEW_DONUT
第一行代码用到了一个属性委托,它由 Navigation 组件库提供,这样写可以简化从 bundle 获取数据的过程。通过它可以在 args
变量中直接找到数据所对应的名称。
如果用户正在编辑一个已有的甜甜圈信息,那么这里的代码会获取该元素的信息,并且使用获取到的信息填充 UI:
if (editingState == EditingState.EXISTING_DONUT) {
donutEntryViewModel.get(args.itemId).observe(
viewLifecycleOwner,
Observer { donutItem ->
binding.name.setText(donutItem.name)
binding.description.setText(donutItem.description)
binding.ratingBar.rating = donutItem.rating.toFloat()
donut = donutItem
}
)
}
需要注意的是这里的代码是从数据库请求信息,并且我们希望整个请求过程能够在 UI 线程之外进行。所以代码里会监听 ViewModel
所提供的 LiveData
对象,并且异步处理请求,当数据返回时填充视图。
当用户点击对话框里的 Done 按钮时,就需要存储用户所输入的信息了。下面这段代码会更新数据库里相应的数据,并且关闭对话框:
binding.doneButton.setOnClickListener {
donutEntryViewModel.addData(
donut?.id ?: 0,
binding.name.text.toString(),
binding.description.text.toString(),
binding.ratingBar.rating.toInt()
)
dismiss()
}
上面的这些代码主要侧重于在目的界面里处理数据,现在我们来看一下如何将数据发送到目标界面。
在 DonutList
中一共有两种途径可以转向对话框。其中一种是当用户点击 悬浮操作按钮
(FAB) 的时候:
binding.fab.setOnClickListener { fabView ->
fabView.findNavController().navigate(DonutListDirections
.actionDonutListToDonutEntryDialogFragment())
}
需要注意的是,这段代码里在创建 NavDirections
对象的时候调用了无参数的构造函数,所以变量会被默认赋值为 -1 (以表明这是一个新的甜甜圈),这也是我们希望通过点击悬浮操作按钮所要实现的效果。
另一个途径是当用户点击列表中已有元素的时候,会打开对话框。可以通过下面的 lambda 表达式实现,它将在 DonutListAdapter
的构建过程中传入 (即 onEdit
参数),然后会在每个表项的 onClick
被触发的时候被调用:
donut ->
findNavController().navigate(DonutListDirections
.actionDonutListToDonutEntryDialogFragment(donut.id))
这里的代码和用户点击悬浮操作按钮的代码相似,只不过这里将表项的 id 传了进去,告诉对话框它要编辑一个已有的元素。而且和我们之前的代码看到的一样,它会用已有元素的信息填充对话框,并且对该表项所做的修改也会相应更新数据库里的对应项。
总结
这就是 SafeArgs 的全部内容。使用起来非常简单 (比起 Bundle 要简单很多),因为依赖库会帮您生成代码来简化数据传递,并且保障了数据类型安全。通过这样的方式,您可以更好地利用数据封装,在目的地之间仅仅传递所需的数据而无需在更大的范围内暴露数据。
请继续关注我们后续的关于导航组件的内容,接下来我们会介绍如何使用 Deep Link。
更多信息
更多关于导航组件的详情,请查看 导航组件使用入门文档
DonutTracker 应用的完整代码,请查看 Github 示例:
更多现代 Android 开发技巧 (MAD Skills) 系列内容,请查看 Android Developers 频道