Android Kotlin 基础知识:03.3 启动外部 activity

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

此 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 应用,以便用户将其游戏结果发送到其他应用并与好友分享结果。

bbdf3ca1c48f3454.png

您的代码需要将参数从一个 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 步:打开并运行起始应用

  1. 为此 Codelab 下载 AndroidTriviaNavigation 起始应用:

如果您已完成上一个向应用添加导航功能的 Codelab,请使用该 Codelab 中的解决方案代码。

从 Android Studio 运行该应用:

  1. 在 Android Studio 中打开该应用。
  2. 在 Android 设备或模拟器上运行应用。该应用是一款知识问答游戏,带有一个抽屉式导航栏,标题屏幕上有一个选项菜单,大多数屏幕的顶部都有一个向上按钮。
  3. 探索该应用,玩玩游戏。如果您通过答对三个问题赢得了游戏,就会看到 Congratulations 屏幕。

632858d6c913739e.png

在此 Codelab 中,您将在 Congratulations 屏幕顶部添加一个分享图标。分享图标让用户可以通过电子邮件或短信分享游戏结果。

第 2 步:将 Safe Args 添加到项目中

  1. 在 Android Studio 中,打开项目级 build.gradle 文件。
  2. 添加 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"

}
  1. 打开应用级 build.gradle 文件。
  2. 在文件的顶部,在所有其他插件之后,添加 apply plugin 语句,其中包含 androidx.navigation.safeargs 插件:
// Adding the apply plugin statement for safeargs
apply plugin: 'androidx.navigation.safeargs'
  1. 重新构建该项目。如果系统提示您安装其他构建工具,请进行安装。

应用项目现在包含生成的 NavDirection 类。

Safe Args 插件会为每个 fragment 生成一个 NavDirection 类。这些类表示应用的所有操作的导航。

例如,GameFragment 现在具有生成的 GameFragmentDirections 类。您可以使用 GameFragmentDirections 类,在游戏 fragment 与应用中的其他 fragment 之间传递类型安全的参数。

如需查看生成的文件,请在 Project > Android 窗格中浏览 generatedJava 文件夹。

第 3 步:将 NavDirection 类添加到游戏 fragment 中

在此步骤中,您将向游戏 fragment 添加 GameFragmentDirections 类。稍后,您将使用此代码在 GameFragment 和游戏状态 fragment(GameWonFragmentGameOverFragment)之间传递参数。

  1. 打开 java 文件夹中的 GameFragment.kt Kotlin 文件。
  2. onCreateView() 方法中,找到游戏胜利条件语句(“We've won!”)。更改已传递到 NavController.navigate() 方法的参数:将游戏胜利状态的操作 ID 替换为使用 GameFragmentDirections 类中的 actionGameFragmentToGameWonFragment() 方法的 ID。

条件语句现在类似于以下代码。您将在下一项任务中为 actionGameFragmentToGameWonFragment() 方法添加参数。

// Using directions to navigate to the GameWonFragment
view.findNavController()
        .navigate(GameFragmentDirections.actionGameFragmentToGameWonFragment())
  1. 同样,找到游戏结束条件语句(“Game over!”)。将游戏结束状态的操作 ID 替换为使用 GameFragmentDirections 类中的游戏结束方法的 ID:
// Using directions to navigate to the GameOverFragment
view.findNavController()
        .navigate(GameFragmentDirections.actionGameFragmentToGameOverFragment())

在此任务中,您将向 gameWonFragment 添加类型安全的参数,并将这些参数安全地传递到 GameFragmentDirections 方法中。同样,您会将其他 fragment 类替换为等效的 NavDirection 类。

第 1 步:向游戏胜利 fragment 添加参数

  1. 打开 res > navigation 文件夹中的 navigation.xml 文件。点击 Design 标签页以打开导航图,您可以在其中设置 fragment 中的参数。
  2. 在预览中,选择 gameWonFragment
  3. Attributes 窗格中,展开 Arguments 部分。
  4. 点击“+”图标,添加参数。将参数命名为 numQuestions,并将类型设置为 Integer,然后点击 Add。此参数表示用户已回答的问题数。ae92865cfaa6371b.png
  5. 选择 gameWonFragment 后,添加第二个参数。将此参数命名为 numCorrect,并将其类型设置为 Integer。此参数表示用户已答对的问题数。664971bc2ea0eebd.png

如果您立即尝试构建该应用,可能会遇到两个编译错误。

No value passed for parameter 'numQuestions'
No value passed for parameter 'numCorrect'

您将在后续步骤中修复这些错误。

第 2 步:传递参数

在此步骤中,您需要将 numQuestionsquestionIndex 参数从 GameFragmentDirections 类传递到 actionGameFragmentToGameWonFragment() 方法中。

  1. 打开 GameFragment.kt Kotlin 文件,并找到游戏胜利条件语句:
else {
 // We've won!  Navigate to the gameWonFragment.
 view.findNavController()
      .navigate(GameFragmentDirections
            .actionGameFragmentToGameWonFragment())
}
  1. numQuestionsquestionIndex 参数传递给 actionGameFragmentToGameWonFragment() 方法:
// Adding the parameters to the Action
view.findNavController()
      .navigate(GameFragmentDirections
            .actionGameFragmentToGameWonFragment(numQuestions, questionIndex))

您可以将问题总数作为 numQuestions 传递,将当前尝试的问题作为 questionIndex 传递。该应用的设计方式是,用户只有在答对所有问题(即已答对的问题数始终等于已回答的问题数)后才能分享其数据。稍后,您可以根据需要更改此游戏逻辑。

  1. GameWonFragment.kt 中,从 Bundle 中提取参数,然后使用 Toast 显示这些参数。将以下代码添加到 onCreateView() 方法中的 return 语句前面:
val args = GameWonFragmentArgs.fromBundle(requireArguments())
Toast.makeText(context, "NumCorrect: ${args.numCorrect}, NumQuestions: ${args.numQuestions}", Toast.LENGTH_LONG).show()
  1. 运行应用,玩玩游戏,确保参数已成功传递给 GameWonFragment。消息框消息会显示在 Congratulations 屏幕上,消息内容为“NumCorrect: 3, NumQuestions: 3”。

不过,您必须先赢得这个知识问答游戏。为降低游戏难度,您可以在 GameFragment.kt Kotlin 文件中将 numQuestions 的值设置为 1,从而将其更改为单问题游戏。

第 3 步:将 fragment 类替换为 NavDirection 类

使用 Safe Args 时,您可以将导航代码中所用的 fragment 类替换为 NavDirection 类。这样一来,您可以将类型安全的参数用于该应用中的其他 fragment。

TitleFragmentGameOverFragmentGameWonFragment 中,更改已传入 navigate() 方法的操作 ID。将操作 ID 替换为相应 NavDirection 类中的等效方法:

  1. 打开 TitleFragment.kt Kotlin 文件。在 onCreateView() 中,在 Play 按钮的点击处理程序中,找到 navigate() 方法。将 TitleFragmentDirections.actionTitleFragmentToGameFragment() 作为该方法的参数传递:
binding.playButton.setOnClickListener { view: View ->
    view.findNavController()
            .navigate(TitleFragmentDirections.actionTitleFragmentToGameFragment())
}
  1. GameOverFragment.kt 文件中,在 Try Again 按钮的点击处理程序中,将 GameOverFragmentDirections.actionGameOverFragmentToGameFragment() 作为 navigate() 方法的参数进行传递:
binding.tryAgainButton.setOnClickListener { view: View ->
    view.findNavController()
        .navigate(GameOverFragmentDirections.actionGameOverFragmentToGameFragment())
}
  1. GameWonFragment.kt 文件中,在 Next Match 按钮的点击处理程序中,将 GameWonFragmentDirections.actionGameWonFragmentToGameFragment() 作为 navigate() 方法的参数进行传递:
binding.nextMatchButton.setOnClickListener { view: View ->
    view.findNavController()
          .navigate(GameWonFragmentDirections.actionGameWonFragmentToGameFragment())
}
  1. 运行应用。

应用的输出内容不会出现任何更改,但现在,该应用已设置完毕,您可以视需要轻松使用 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_VIEWACTION_EDITACTION_DIAL)在 Intent 类中定义。

如需详细了解隐式 intent,请参阅将用户转到其他应用

第 1 步:在“Congratulations”屏幕中添加选项菜单

  1. 打开 GameWonFragment.kt Kotlin 文件。
  2. onCreateView() 方法的 return 之前,调用 setHasOptionsMenu() 方法,并传入 true
  setHasOptionsMenu(true)

第 2 步:构建和调用隐式 intent

通过修改代码来构建和调用 Intent,发送有关用户游戏数据的消息。由于多个不同的应用都可以处理 ACTION_SEND intent,因此用户会看到一个选择器,供其选择要如何发送信息。

  1. 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
}
  1. getShareIntent() 方法下方,创建 shareSuccess() 方法。此方法会从 getShareIntent() 获取 Intent,并调用 startActivity(),以开始分享。
// Starting an Activity with our new Intent
private fun shareSuccess() {
   startActivity(getShareIntent())
}
  1. 起始代码已包含 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
       }
}
  1. 如需处理该菜单项,请替换 GameWonFragment 中的 onOptionsItemSelected()。在点击该菜单项时调用 shareSuccess() 方法:
// Sharing from the Menu
override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId){
            R.id.share -> shareSuccess()
        }
        return super.onOptionsItemSelected(item)
}
  1. 现在运行您的应用。(您可能需要将一些软件包导入 GameWonFragment.kt,然后代码才会运行。)赢得游戏后,请注意应用栏右上角显示的分享图标。点击分享图标,分享关于您获胜的消息。

5ab64a083db8844e.png bbdf3ca1c48f3454.png

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 着陆页