此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。
在上一个 Codelab 中,您修改 AndroidTrivia 应用,向该应用添加了导航功能。在此 Codelab 中,您将修改该应用,使用户可以分享他们的游戏结果。用户可以发送电子邮件或短信,或者将游戏结果复制到剪贴板。
您应当已掌握的内容
- Kotlin 的基础知识
- 如何使用 Kotlin 创建基本 Android 应用
学习内容
- 如何使用
Bundle
类将参数从一个 fragment 传递到另一个 fragment - 如何使用 Safe Args Gradle 插件确保类型安全
- 如何向应用添加“分享”菜单项
- 什么是隐式 intent,以及如何创建隐式 intent
实践内容
- 修改 AndroidTrivia 代码以使用 Safe Args 插件,该插件会生成
NavDirection
类。 - 使用生成的
NavDirection
类中的一个,在GameFragment
和游戏状态 fragment 之间传递类型安全的参数。 - 向应用添加“分享”菜单项。
- 创建一个隐式 intent,用于启动一个选择器,供用户用来分享有关其游戏结果的消息。
您在前两个 Codelab 中使用的 AndroidTrivia 应用是一款游戏,用户可以在其中回答与 Android 开发相关的问题。如果用户答对三个问题,就会赢得游戏。
下载 AndroidTriviaNavigation 起始代码;或者,如果您成功完成了上一个 Codelab,请使用相应代码作为此 Codelab 的起始代码。
在此 Codelab 中,您将更新 AndroidTrivia 应用,以便用户将其游戏结果发送到其他应用并与好友分享结果。
您的代码需要将参数从一个 fragment 传递到另一个 fragment,然后用户才能在 AndroidTrivia 应用中分享其游戏结果。为防止这些事务中出现 bug,并确保类型安全,您可以使用名为 Safe Args 的 Gradle 插件。该插件会生成 NavDirection
类,您需要将这些类添加到代码中。
在此 Codelab 的后续任务中,您将使用生成的 NavDirection
类在 fragment 之间传递参数。
为什么需要 Safe Args 插件
通常,您的应用需要在 fragment 之间传递数据。如需将数据从一个 fragment 传递到另一个 fragment,一种方式是使用 Bundle
类的实例。Android Bundle
是一个键值对存储区。
键值对存储区(也称为“字典”或“关联数组”)是一种数据结构,在其中,您使用唯一键(字符串)获取与该键相关联的值。例如:
键 | 值 |
"name" | "Anika" |
"favorite_weather" | "sunny" |
"favorite_color" | "blue" |
您的应用可以使用 Bundle
将数据从 fragment A 传递到 fragment B。例如,fragment A 创建了一个 Bundle,将信息保存为键值对,然后将 Bundle
传递到 fragment B。接下来,fragment B 使用键从 Bundle
提取键值对。这种方法行得通,但它会生成可以编译的代码,随后可能会在应用运行时引发错误。
可能出现的几类错误包括:
- 类型不匹配错误。例如,如果 fragment A 发送了一个字符串,而 fragment B 请求 Bundle 提供一个整数,则请求会返回默认值零。由于零是有效值,因此在编译应用时,这种类型不匹配问题不会抛出错误。不过,当用户运行应用时,该错误可能会导致应用出现异常或崩溃。
- 键缺失错误。如果 fragment B 请求未在 Bundle 中设置的参数,则操作会返回
null
。同样,这在应用编译时不会抛出错误,但在用户运行应用时可能会导致严重问题。
您需要在 Android Studio 中编译应用时捕获这些错误,然后再将应用部署到生产环境中。换句话说,您需要在应用开发期间捕获错误,以免用户遇到这些错误。
为帮助解决这些问题,Android 导航架构组件中添加了一项名为 Safe Args 的功能。Safe Args 是一个 Gradle 插件,该插件可以生成代码和类,帮助在编译时检测那些在应用运行前可能不会出现的错误。
第 1 步:打开并运行起始应用
- 为此 Codelab 下载 AndroidTriviaNavigation 起始应用:
如果您已完成上一个向应用添加导航功能的 Codelab,请使用该 Codelab 中的解决方案代码。
从 Android Studio 运行该应用:
- 在 Android Studio 中打开该应用。
- 在 Android 设备或模拟器上运行应用。该应用是一款知识问答游戏,带有一个抽屉式导航栏,标题屏幕上有一个选项菜单,大多数屏幕的顶部都有一个向上按钮。
- 探索该应用,玩玩游戏。如果您通过答对三个问题赢得了游戏,就会看到 Congratulations 屏幕。
在此 Codelab 中,您将在 Congratulations 屏幕顶部添加一个分享图标。分享图标让用户可以通过电子邮件或短信分享游戏结果。
第 2 步:将 Safe Args 添加到项目中
- 在 Android Studio 中,打开项目级
build.gradle
文件。 - 添加
navigation-safe-args-gradle-plugin
依赖项,如下所示:
// Adding the safe-args dependency to the project Gradle file
dependencies {
...
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
}
- 打开应用级
build.gradle
文件。 - 在文件的顶部,在所有其他插件之后,添加
apply plugin
语句,其中包含androidx.navigation.safeargs
插件:
// Adding the apply plugin statement for safeargs
apply plugin: 'androidx.navigation.safeargs'
- 重新构建该项目。如果系统提示您安装其他构建工具,请进行安装。
应用项目现在包含生成的 NavDirection
类。
Safe Args 插件会为每个 fragment 生成一个 NavDirection
类。这些类表示应用的所有操作的导航。
例如,GameFragment
现在具有生成的 GameFragmentDirections
类。您可以使用 GameFragmentDirections
类,在游戏 fragment 与应用中的其他 fragment 之间传递类型安全的参数。
如需查看生成的文件,请在 Project > Android 窗格中浏览 generatedJava 文件夹。
第 3 步:将 NavDirection 类添加到游戏 fragment 中
在此步骤中,您将向游戏 fragment 添加 GameFragmentDirections
类。稍后,您将使用此代码在 GameFragment
和游戏状态 fragment(GameWonFragment
和 GameOverFragment
)之间传递参数。
- 打开 java 文件夹中的
GameFragment.kt
Kotlin 文件。 - 在
onCreateView()
方法中,找到游戏胜利条件语句(“We've won!”)。更改已传递到NavController.navigate()
方法的参数:将游戏胜利状态的操作 ID 替换为使用GameFragmentDirections
类中的actionGameFragmentToGameWonFragment()
方法的 ID。
条件语句现在类似于以下代码。您将在下一项任务中为 actionGameFragmentToGameWonFragment()
方法添加参数。
// Using directions to navigate to the GameWonFragment
view.findNavController()
.navigate(GameFragmentDirections.actionGameFragmentToGameWonFragment())
- 同样,找到游戏结束条件语句(“Game over!”)。将游戏结束状态的操作 ID 替换为使用
GameFragmentDirections
类中的游戏结束方法的 ID:
// Using directions to navigate to the GameOverFragment
view.findNavController()
.navigate(GameFragmentDirections.actionGameFragmentToGameOverFragment())
在此任务中,您将向 gameWonFragment
添加类型安全的参数,并将这些参数安全地传递到 GameFragmentDirections
方法中。同样,您会将其他 fragment 类替换为等效的 NavDirection
类。
第 1 步:向游戏胜利 fragment 添加参数
- 打开 res > navigation 文件夹中的
navigation.xml
文件。点击 Design 标签页以打开导航图,您可以在其中设置 fragment 中的参数。 - 在预览中,选择 gameWonFragment。
- 在 Attributes 窗格中,展开 Arguments 部分。
- 点击“+”图标,添加参数。将参数命名为
numQuestions
,并将类型设置为 Integer,然后点击 Add。此参数表示用户已回答的问题数。 - 选择 gameWonFragment 后,添加第二个参数。将此参数命名为 numCorrect,并将其类型设置为 Integer。此参数表示用户已答对的问题数。
如果您立即尝试构建该应用,可能会遇到两个编译错误。
No value passed for parameter 'numQuestions'
No value passed for parameter 'numCorrect'
您将在后续步骤中修复这些错误。
第 2 步:传递参数
在此步骤中,您需要将 numQuestions
和 questionIndex
参数从 GameFragmentDirections
类传递到 actionGameFragmentToGameWonFragment()
方法中。
- 打开
GameFragment.kt
Kotlin 文件,并找到游戏胜利条件语句:
else {
// We've won! Navigate to the gameWonFragment.
view.findNavController()
.navigate(GameFragmentDirections
.actionGameFragmentToGameWonFragment())
}
- 将
numQuestions
和questionIndex
参数传递给actionGameFragmentToGameWonFragment()
方法:
// Adding the parameters to the Action
view.findNavController()
.navigate(GameFragmentDirections
.actionGameFragmentToGameWonFragment(numQuestions, questionIndex))
您可以将问题总数作为 numQuestions
传递,将当前尝试的问题作为 questionIndex
传递。该应用的设计方式是,用户只有在答对所有问题(即已答对的问题数始终等于已回答的问题数)后才能分享其数据。稍后,您可以根据需要更改此游戏逻辑。
- 在
GameWonFragment.kt
中,从 Bundle 中提取参数,然后使用Toast
显示这些参数。将以下代码添加到onCreateView()
方法中的return
语句前面:
val args = GameWonFragmentArgs.fromBundle(requireArguments())
Toast.makeText(context, "NumCorrect: ${args.numCorrect}, NumQuestions: ${args.numQuestions}", Toast.LENGTH_LONG).show()
- 运行应用,玩玩游戏,确保参数已成功传递给
GameWonFragment
。消息框消息会显示在 Congratulations 屏幕上,消息内容为“NumCorrect: 3, NumQuestions: 3”。
不过,您必须先赢得这个知识问答游戏。为降低游戏难度,您可以在 GameFragment.kt
Kotlin 文件中将 numQuestions
的值设置为 1
,从而将其更改为单问题游戏。
第 3 步:将 fragment 类替换为 NavDirection 类
使用 Safe Args 时,您可以将导航代码中所用的 fragment 类替换为 NavDirection
类。这样一来,您可以将类型安全的参数用于该应用中的其他 fragment。
在 TitleFragment
、GameOverFragment
和 GameWonFragment
中,更改已传入 navigate()
方法的操作 ID。将操作 ID 替换为相应 NavDirection
类中的等效方法:
- 打开
TitleFragment.kt
Kotlin 文件。在onCreateView()
中,在 Play 按钮的点击处理程序中,找到navigate()
方法。将TitleFragmentDirections.actionTitleFragmentToGameFragment()
作为该方法的参数传递:
binding.playButton.setOnClickListener { view: View ->
view.findNavController()
.navigate(TitleFragmentDirections.actionTitleFragmentToGameFragment())
}
- 在
GameOverFragment.kt
文件中,在 Try Again 按钮的点击处理程序中,将GameOverFragmentDirections.actionGameOverFragmentToGameFragment()
作为navigate()
方法的参数进行传递:
binding.tryAgainButton.setOnClickListener { view: View ->
view.findNavController()
.navigate(GameOverFragmentDirections.actionGameOverFragmentToGameFragment())
}
- 在
GameWonFragment.kt
文件中,在 Next Match 按钮的点击处理程序中,将GameWonFragmentDirections.actionGameWonFragmentToGameFragment()
作为navigate()
方法的参数进行传递:
binding.nextMatchButton.setOnClickListener { view: View ->
view.findNavController()
.navigate(GameWonFragmentDirections.actionGameWonFragmentToGameFragment())
}
- 运行应用。
应用的输出内容不会出现任何更改,但现在,该应用已设置完毕,您可以视需要轻松使用 NavDirection
类传递参数。
在此任务中,您将向该应用添加分享功能,以便用户分享其游戏结果。这通过使用 Android intent(具体而言是一个隐式 intent)来实现。分享功能可通过 GameWonFragment
类中的选项菜单来访问。在应用界面中,菜单项将在 Congratulations 屏幕顶部显示为分享图标。
隐式 intent
到目前为止,您已使用导航组件在 activity 内的 fragment 之间导航。Android 还允许您使用 intent 导航到其他应用提供的 activity。您可在 AndroidTrivia 应用中利用这项功能,让用户可以与好友分享游戏结果。
Intent
是一个简单的消息对象,用于在 Android 组件之间进行通信。intent 分为两种类型:显式和隐式。您可以使用显式 intent 向特定目标发送消息。而借助隐式 intent,您可以启动一个 activity,而无需了解将由哪个应用或 activity 处理该任务。例如,如果您需要应用拍摄照片,您通常不会关心要由哪个应用或 activity 执行这个任务。如果多个 Android 应用可以处理同一隐式 intent,Android 会向用户显示选择器,以便用户选择使用哪个应用来处理请求。
每个隐式 intent 都必须具有 ACTION
,用于描述待完成的操作类型。常见操作(例如 ACTION_VIEW
、ACTION_EDIT
和 ACTION_DIAL
)在 Intent
类中定义。
如需详细了解隐式 intent,请参阅将用户转到其他应用。
第 1 步:在“Congratulations”屏幕中添加选项菜单
- 打开
GameWonFragment.kt
Kotlin 文件。 - 在
onCreateView()
方法的return
之前,调用setHasOptionsMenu()
方法,并传入true
:
setHasOptionsMenu(true)
第 2 步:构建和调用隐式 intent
通过修改代码来构建和调用 Intent
,发送有关用户游戏数据的消息。由于多个不同的应用都可以处理 ACTION_SEND
intent,因此用户会看到一个选择器,供其选择要如何发送信息。
- 在
GameWonFragment
类的onCreateView()
方法之后,创建一个名为getShareIntent()
的私有方法,如下所示。用于为args
设置值的代码行与类的onCreateView()
中所用的代码行相同。
在该方法的其余代码中,您将构建 ACTION_SEND
intent 来传送用户想要分享的消息。数据的 MIME 类型由 setType()
方法指定。待传送的实际数据在 EXTRA_TEXT
中指定。(share_success_text
字符串是在 strings.xml
资源文件中定义的。)
// Creating our Share Intent
private fun getShareIntent() : Intent {
val args = GameWonFragmentArgs.fromBundle(requireArguments())
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.setType("text/plain")
.putExtra(Intent.EXTRA_TEXT, getString(R.string.share_success_text, args.numCorrect, args.numQuestions))
return shareIntent
}
- 在
getShareIntent()
方法下方,创建shareSuccess()
方法。此方法会从getShareIntent()
获取Intent
,并调用startActivity()
,以开始分享。
// Starting an Activity with our new Intent
private fun shareSuccess() {
startActivity(getShareIntent())
}
- 起始代码已包含
winner_menu.xml
菜单文件。替换GameWonFragment
类中的onCreateOptionsMenu()
,以膨胀winner_menu
。
使用 getShareIntent()
获取 shareIntent
。为确保 shareIntent
解析为 Activity
,请使用 Android 软件包管理器 ( PackageManager
) 进行检查,它会记录设备上安装的应用和 activity。使用 activity 的 packageManager
属性获取对软件包管理器的访问权限,并调用 resolveActivity()
。如果结果为 null
,这表示 shareIntent
无法解析,请从膨胀的菜单中找到分享菜单项,并将该菜单项设为不可见。
// Showing the Share Menu Item Dynamically
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.winner_menu, menu)
if(getShareIntent().resolveActivity(requireActivity().packageManager)==null){
menu.findItem(R.id.share).isVisible = false
}
}
- 如需处理该菜单项,请替换
GameWonFragment
类中的onOptionsItemSelected()
。在点击该菜单项时调用shareSuccess()
方法:
// Sharing from the Menu
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId){
R.id.share -> shareSuccess()
}
return super.onOptionsItemSelected(item)
}
- 现在运行您的应用。(您可能需要将一些软件包导入
GameWonFragment.kt
,然后代码才会运行。)赢得游戏后,请注意应用栏右上角显示的分享图标。点击分享图标,分享关于您获胜的消息。
Android Studio 项目:AndroidTrivia-Solution
Safe Args:
- 为帮助捕获在将数据从一个 fragment 传递到另一个 fragment 时因键缺失或类型不匹配而导致的错误,请使用名为 Safe Args 的 Gradle 插件。
- 对于应用中的每个 fragment,Safe Args 插件都会生成相应的
NavDirection
类。您需要将NavDirection
类添加到 fragment 代码中,然后使用该类在相应 fragment 和其他 fragment 之间传递参数。 NavDirection
类表示应用的所有操作的导航。
隐式 intent:
- 隐式 intent 会声明您的应用希望其他某个应用(例如相机应用或电子邮件应用)代表其执行的操作。
- 如果多个 Android 应用都可以处理某个隐式 intent,Android 会向用户显示一个选择器。例如,用户在 AndroidTrivia 应用中点按分享图标后,可以选择要用哪个应用来分享游戏结果。
- 如需构建 intent,您需要声明要执行的操作,例如
ACTION_SEND
。 - 多个
Intent()
构造函数可帮助您构建 intent。
分享功能:
- 在与好友分享自己的获胜消息时,
Intent
操作将为Intent.ACTION_SEND.
- 如需为 fragment 添加选项菜单,请在 fragment 代码中将
setHasOptionsMenu()
方法设置为true
。 - 在 fragment 代码中,替换
onCreateOptionsMenu()
方法以膨胀菜单。 - 替换
onOptionsItemSelected()
,以使用startActivity()
将Intent
发送到可以处理它的其他应用。
当用户点按菜单项时,将触发相应 intent,用户会看到 SEND
操作的选择器。
Udacity 课程:
Android 开发者文档:
此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:
- 根据需要布置作业。
- 告知学生如何提交家庭作业。
- 给家庭作业评分。
讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。
如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。
回答以下问题
问题 1
如果从 fragment A 传递参数到 fragment B,但未使用 Safe Args 来确保参数是类型安全的,可能会出现以下哪些问题,进而可能导致应用在运行时发生崩溃?请选择所有适用的选项。
- fragment A 没有向 fragment B 发送其请求的数据。
- fragment A 可能会发送 fragment B 未请求的数据。
- fragment A 发送的数据类型可能与 fragment B 需要的类型不同。例如,fragment A 可能会发送字符串,而 fragment B 请求的是整数,因而导致返回值为零。
- fragment A 使用的参数名称与 fragment B 请求的名称不同。
问题 2
Safe Args Gradle 插件有什么作用?请选择所有适用的选项:
- 生成简单的对象和构建器类,以便对为目的地和操作指定的参数进行类型安全的访问。
- 创建可通过修改来简化 fragment 之间的参数传递的
Navigation
类。 - 确保您无需在代码中使用 Android Bundle。
- 为您在导航图中定义的每个操作生成一个方法。
- 防止代码使用错误的键从 Bundle 中提取数据。
问题 3
什么是隐式 intent?
- 一项由您的应用在其一个 fragment 中启动,并在另一个 fragment 中完成的任务。
- 一项应用始终通过向用户显示选择器来完成的任务。
- 一项由您的应用启动,但不知道将由哪个应用或 activity 处理的任务。
- 隐式 intent 就是您在导航图中的目的地之间设置的操作。
开始学习下一课:
如需本课程中其他 Codelab 的链接,请参阅“Android Kotlin 基础知识”Codelab 着陆页。