Android Kotlin 基础知识 05.3:使用 ViewModel 和 LiveData 绑定数据

此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。

简介

在本课程之前的 Codelab 中,您改进了 GuessTheWord 应用的代码。该应用现在使用 ViewModel 对象,因此应用数据可在设备配置更改(如屏幕旋转和键盘可用性的变化)后继续存在。此外,您还添加了可观察的 LiveData,因此当观察到的数据发生变化时,视图会自动收到通知。

在此 Codelab 中,您可以继续使用 GuessTheWord 应用。您将视图绑定到应用中的 ViewModel 类,以便布局中的视图直接与 ViewModel 对象通信。(到目前为止,在您的应用中,视图已通过应用的 fragment 与 ViewModel 间接通信)。将数据绑定与 ViewModel 对象集成后,您便不再需要应用的 fragment 中的点击处理程序,因此可以将其移除。

此外,您还将 GuessTheWord 应用更改为使用 LiveData 作为数据绑定来源,在不使用 LiveData 观察器方法的情况下,将数据变化通知给界面。

您应当已掌握的内容

  • 如何使用 Kotlin 创建基本的 Android 应用?
  • activity 和 fragment 生命周期如何工作。
  • 如何在应用中使用 ViewModel 对象?
  • 如何使用 LiveDataViewModel 中存储数据?
  • 如何添加观察器方法来观察 LiveData 数据的变化。

学习内容

  • 如何使用数据绑定库的元素。
  • 如何将 ViewModel 与数据绑定集成。
  • 如何将 LiveData 与数据绑定集成。
  • 如何使用监听器绑定替换 Fragment 中的点击监听器。
  • 如何将字符串格式添加到数据绑定表达式?

您将执行的操作

  • GuessTheWord 布局中的视图与 ViewModel 对象间接通信,同时使用界面控制器(fragment)来中继信息。在本 Codelab 中,您需要将应用的视图绑定到 ViewModel 对象,以便这些视图与 ViewModel 对象直接通信。
  • 您将更改应用以使用 LiveData 作为数据绑定来源。完成此项更改后,LiveData 对象会将数据变化通知给界面,而不再需要 LiveData 观察器方法。

在第 5 课 Codelab 中,您将开发 GuessTheWord 应用(从起始代码开始)。GuessTheWord 是一款双人角色扮演式游戏,玩家必须相互协作才能尽可能赢得最高分。

第一个玩家会检查应用中的单词,然后轮流执行每个单词,确保不会向第二个玩家显示单词。第二个玩家猜词。

玩游戏时,第一个玩家在设备上打开应用并看到“吉他”字样,如以下屏幕截图所示。

第一个玩家会说出单词,注意不要实际说出单词。

  • 当第二个玩家猜出这个单词后,第一个玩家会按 Got It(知道了)按钮,这会将计数增加 1 并显示下一个单词。
  • 如果第二个玩家猜不出单词,第一个玩家会按 Skip 按钮,这会将计数减少 1,并跳至下一个单词。
  • 如需结束游戏,请按结束游戏按钮。(本系列第一个 Codelab 的起始代码中不包含此功能。)

在此 Codelab 中,您可以将数据绑定与 LiveData 集成到 ViewModel 对象中,从而改进 GuessTheWord 应用。这样可自动执行布局中的视图与 ViewModel 对象之间的通信,并可使用 LiveData 简化代码。

广告标题画面

游戏屏幕

分数屏幕

在此任务中,您将找到并运行此 Codelab 的起始代码。您可以使用在上一个 Codelab 中构建的 GuessTheWord 应用作为起始代码,也可以下载起始应用。

  1. (可选)如果您未使用上一个 Codelab 中的代码,请下载此 Codelab 的起始代码。解压缩代码,然后在 Android Studio 中打开项目。
  2. 运行应用并玩游戏。
  3. 请注意,Got It(知道)按钮会显示下一个单词并将得分增加 1,而 Skip 按钮显示下一个单词并将得分降低 1。点击结束游戏按钮即可结束游戏。
  4. 循环浏览所有字词,您会发现应用会自动导航到该得分屏幕。

在上一个 Codelab 中,您已使用数据绑定访问 GuessTheWord 应用中的视图,但数据绑定的真正作用是顾名思义:直接将数据绑定到应用中的视图对象。

当前应用架构

在您的应用中,视图在 XML 布局中定义,这些视图的数据保存在 ViewModel 对象中。每个视图与其对应的 ViewModel 之间都有一个界面控制器,它们充当它们之间的中继。

例如:

  • Got It 按钮在 game_fragment.xml 布局文件中被定义为 Button 视图。
  • 当用户点按 Got It 按钮时,GameFragment fragment 中的点击监听器会调用 GameViewModel 中的相应点击监听器。
  • 分数会在 GameViewModel 中更新。

Button 视图和 GameViewModel 不直接通信 - 它们需要 GameFragment 中的点击监听器。

传递到数据绑定的 ViewModel

如果布局中的视图直接与 ViewModel 对象中的数据进行通信(而不依赖于界面控制器作为中间层),则会更简单。

ViewModel 对象保存 GuessTheWord 应用中的所有界面数据。通过将 ViewModel 对象传递到数据绑定,您可以自动执行视图与 ViewModel 对象之间的某些通信。

在此任务中,您要将 GameViewModelScoreViewModel 类与其对应的 XML 布局相关联。此外,您还可以设置监听器绑定来处理点击事件。

第 1 步:为 GameViewModel 添加数据绑定

在此步骤中,您需要将 GameViewModel 与对应的布局文件 game_fragment.xml 相关联。

  1. game_fragment.xml 文件中,添加 GameViewModel 类型的数据绑定变量。如果 Android Studio 中存在错误,请清理并重建项目。
<layout ...>

   <data>

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  
   <androidx.constraintlayout...
  1. GameFragment 文件中,将 GameViewModel 传入数据绑定。

    为此,请将 viewModel 分配给您在上一步中声明的 binding.gameViewModel 变量。将此代码放置在 onCreateView() 中,并初始化 viewModel。如果 Android Studio 中存在错误,请清理并重建项目。
// Set the viewmodel for databinding - this allows the bound layout access 
// to all the data in the ViewModel
binding.gameViewModel = viewModel

第 2 步:使用监听器绑定处理事件

监听器绑定是在触发 onClick()onZoomIn()onZoomOut() 等事件时运行的绑定表达式。监听器绑定编写为 lambda 表达式。

数据绑定会创建一个监听器,并在视图上设置该监听器。当发生监听事件时,监听器会评估 lambda 表达式。监听器绑定适用于 Android Gradle 插件 2.0 或更高版本。如需了解详情,请参阅布局和绑定表达式

在此步骤中,您需要将 GameFragment 文件中的点击监听器替换为 game_fragment.xml 文件中的监听器绑定。

  1. game_fragment.xml 中,将 onClick 属性添加到 skip_button 中。定义绑定表达式并在 GameViewModel 中调用 onSkip() 方法。此绑定表达式称为“监听器绑定”。
<Button
   android:id="@+id/skip_button"
   ...
   android:onClick="@{() -> gameViewModel.onSkip()}"
   ... />
  1. 同样,将 correct_button 的点击事件绑定到 GameViewModel 中的 onCorrect() 方法。
<Button
   android:id="@+id/correct_button"
   ...
   android:onClick="@{() -> gameViewModel.onCorrect()}"
   ... />
  1. end_game_button 的点击事件绑定到 GameViewModel 中的 onGameFinish() 方法。
<Button
   android:id="@+id/end_game_button"
   ...
   android:onClick="@{() -> gameViewModel.onGameFinish()}"
   ... />
  1. GameFragment 中,移除设置点击监听器的语句,并移除点击监听器调用的函数。您不再需要这些。

要移除的代码:

binding.correctButton.setOnClickListener { onCorrect() }
binding.skipButton.setOnClickListener { onSkip() }
binding.endGameButton.setOnClickListener { onEndGame() }

/** Methods for buttons presses **/
private fun onSkip() {
   viewModel.onSkip()
}
private fun onCorrect() {
   viewModel.onCorrect()
}
private fun onEndGame() {
   gameFinished()
}

第 3 步:为 ScoreViewModel 添加数据绑定

在此步骤中,您需要将 ScoreViewModel 与对应的布局文件 score_fragment.xml 相关联。

  1. score_fragment.xml 文件中,添加 ScoreViewModel 类型的绑定变量。此步骤与上面针对 GameViewModel 执行的操作类似。
<layout ...>
   <data>
       <variable
           name="scoreViewModel"
           type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
   </data>
   <androidx.constraintlayout.widget.ConstraintLayout
  1. score_fragment.xml 中,将 onClick 属性添加到 play_again_button 中。定义监听器绑定并在 ScoreViewModel 中调用 onPlayAgain() 方法。
<Button
   android:id="@+id/play_again_button"
   ...
   android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
   ... />
  1. ScoreFragment 中的 onCreateView() 内,初始化 viewModel。然后,初始化 binding.scoreViewModel 绑定变量。
viewModel = ...
binding.scoreViewModel = viewModel
  1. ScoreFragment 中,移除为 playAgainButton 设置点击监听器的代码。如果 Android Studio 显示错误,请清理并重建项目。

要移除的代码:

binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. 运行应用。应用应该像以前一样工作,但现在按钮视图直接与 ViewModel 对象通信。这些视图不再通过 ScoreFragment 中的按钮点击处理程序进行通信。

排查数据绑定错误消息

当应用使用数据绑定时,编译过程会生成用于数据绑定的中间类。在您试图编译应用之前,Android Studio 检测不到的可能是应用错误,因此在编写代码时看不到警告或红色代码。但在编译时,却出现生成的中间类存在的隐性错误。

如果您收到一条不明错误消息,请执行以下操作:

  1. 仔细查看 Android Studio 的 Build 窗格中的消息。如果您看到以 databinding 结尾的位置,表示数据绑定时出错。
  2. 在布局 XML 文件中,检查使用数据绑定的 onClick 属性中是否存在错误。查找 lambda 表达式调用的函数,并确保其存在。
  3. 在 XML 的 <data> 部分中,检查数据绑定变量的拼写。

例如,请注意以下属性值中函数名称 onCorrect() 的拼写错误:

android:onClick="@{() -> gameViewModel.onCorrectx()}"

另请注意 XML 文件中 <data> 部分的 gameViewModel 的拼写错误:

<data>
   <variable
       name="gameViewModelx"
       type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>

在您编译应用后,Android Studio 不会检测此类错误,并且编译器会显示如下错误消息:

error: cannot find symbol
import com.example.android.guesstheword.databinding.GameFragmentBindingImpl"

symbol:   class GameFragmentBindingImpl
location: package com.example.android.guesstheword.databinding

数据绑定适用于与 ViewModel 对象搭配使用的 LiveData。现在,您已经向 ViewModel 对象添加了数据绑定,可以添加 LiveData 了。

在此任务中,您将更改 GuessTheWord 应用,将 LiveData 用作数据绑定来源,以在数据发生变化时通知界面,而无需使用 LiveData 观察器方法。

第 1 步:将 LiveData 单词添加到 game_fragment.xml 文件中

在这一步中,您将当前单词文本视图直接绑定到 ViewModel 中的 LiveData 对象。

  1. game_fragment.xml 中,将 android:text 属性添加到 word_text 文本视图。

使用绑定变量 gameViewModel 将其设置为 GameViewModel 对象中的 LiveData 对象 word

<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{gameViewModel.word}"
   ... />

请注意,您不必使用 word.value,您可以改为使用实际的 LiveData 对象。LiveData 对象会显示 word 的当前值。如果 word 的值为 null,LiveData 对象会显示空字符串。

  1. GameFragment 中的 onCreateView() 内,初始化 gameViewModel 后,将当前 activity 设置为 binding 变量的生命周期所有者。以上代码定义了上述 LiveData 对象的范围,允许该对象自动更新 game_fragment.xml 布局中的视图。
binding.gameViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
  1. GameFragment 中,移除 LiveData word 的观察器。

要移除的代码:

/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
   binding.wordText.text = newWord
})
  1. 运行应用并玩游戏。现在,界面字词中更新了观察器方法,但没有观察者方法。

第 2 步:在得分_fragment.xml 文件中添加得分 LiveData

在此步骤中,您需要将 LiveData score 绑定到得分 fragment 中的得分文本视图。

  1. score_fragment.xml 中,将 android:text 属性添加到分数文本视图。将 scoreViewModel.score 分配给 text 属性。由于 score 是一个整数,因此请使用 String.valueOf() 将其转换为字符串。
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{String.valueOf(scoreViewModel.score)}"
   ... />
  1. ScoreFragment 中,初始化 scoreViewModel 后,将当前 activity 设置为 binding 变量的生命周期所有者。
binding.scoreViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
  1. ScoreFragment 中,移除 score 对象的观察器。

要移除的代码:

// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. 运行应用并玩游戏。请注意,在得分 Fragment 中,得分观察器会正确显示该得分,但没有观察者。

第 3 步:使用数据绑定添加字符串格式

在布局中,您可以添加字符串格式和数据绑定。在此任务中,您将设置当前单词的格式以为其添加引号。您还可以设置分数字符串的格式,为其添加“当前得分”前缀,如下图所示。

  1. string.xml 中,添加以下字符串,用于设置 wordscore 文本视图的格式。%s%d 是当前单词和当前分数的占位符。
<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>
  1. game_fragment.xml 中,更新 word_text 文本视图的 text 属性以使用 quote_format 字符串资源。传入 gameViewModel.word。这会将当前字词作为参数传递给格式字符串。
<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{@string/quote_format(gameViewModel.word)}"
   ... />
  1. 设置 score 文本视图的格式,类似于 word_text。在 game_fragment.xml 中,将 text 属性添加到 score_text 文本视图。使用字符串资源 score_format,它接受一个以 %d 占位符表示的数字参数。将 LiveData 对象 score 作为参数传入此格式字符串。
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{@string/score_format(gameViewModel.score)}"
   ... />
  1. GameFragment 类的 onCreateView() 方法内,移除 score 观察器代码。

要移除的代码:

viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. 清理、重建和运行应用,然后玩游戏。请注意,当前单词和得分在游戏屏幕上设置了格式。

恭喜!您已将 LiveDataViewModel 与应用中的数据绑定相集成。这样,布局中的视图就可以直接与 ViewModel 通信,而无需在 fragment 中使用点击处理程序。此外,您还使用 LiveData 对象作为数据绑定来源,在没有 LiveData 观察器方法的情况下,自动将数据变化通知给界面。

Android Studio 项目:GuessTheWord

  • 数据绑定库可与 ViewModelLiveData 等 Android 架构组件无缝配合使用。
  • 应用中的布局可以绑定到架构组件中的数据,这些数据已经可以帮助您管理界面控制器的生命周期并接收有关数据变化的通知。

ViewModel 数据绑定

  • 您可以使用数据绑定将 ViewModel 与布局相关联。
  • ViewModel 对象用于存储界面数据。通过将 ViewModel 对象传递到数据绑定,您可以自动执行视图与 ViewModel 对象之间的某些通信。

如何将 ViewModel 与布局相关联:

  • 在布局文件中,添加类型为 ViewModel 的数据绑定变量。
   <data>

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  • GameFragment 文件中,将 GameViewModel 传入数据绑定。
binding.gameViewModel = viewModel

监听器绑定

  • 监听器绑定是触发 onClick() 等点击事件时在布局中运行的绑定表达式。
  • 监听器绑定编写为 lambda 表达式。
  • 借助监听器绑定,您可以将界面控制器中的点击监听器替换为布局文件中的监听器绑定。
  • 数据绑定会创建一个监听器,并在视图上设置该监听器。
 android:onClick="@{() -> gameViewModel.onSkip()}"

将 LiveData 添加到数据绑定

  • LiveData 对象可以用作数据绑定来源,自动将数据变化通知给界面。
  • 您可以将视图直接绑定到 ViewModel 中的 LiveData 对象。当 ViewModel 中的 LiveData 发生变化时,布局中的视图可以自动更新,界面控制器中不会出现观察者方法。
android:text="@{gameViewModel.word}"
  • 如需使 LiveData 数据绑定正常运行,请在界面控制器中将当前 activity(界面控制器)设置为 binding 变量的生命周期所有者。
binding.lifecycleOwner = this

使用数据绑定设置字符串格式

  • 您可以使用数据绑定来设置字符串资源的格式,例如使用 %s 表示字符串,使用 %d 表示整数。
  • 要更新视图的 text 属性,请将 LiveData 对象作为参数传入格式字符串。
 android:text="@{@string/quote_format(gameViewModel.word)}"

Udacity 课程:

Android 开发者文档:

此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:

  • 根据需要布置作业。
  • 告知学生如何提交家庭作业。
  • 给家庭作业评分。

讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。

如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。

回答以下问题

问题 1

在以下关于监听器绑定的表述中,哪一项是不正确的?

  • 监听器绑定是在事件发生时运行的绑定表达式。
  • 监听器绑定适用于所有版本的 Android Gradle 插件。
  • 监听器绑定编写为 lambda 表达式。
  • 监听器绑定类似于方法引用,但允许您运行任意数据绑定表达式。

问题 2

假设您的应用包含以下字符串资源:
<string name="generic_name">Hello %s</string>

以下哪一项是使用数据绑定表达式设置字符串格式的正确语法?

  • android:text= "@{@string/generic_name(user.name)}"
  • android:text= "@{string/generic_name(user.name)}"
  • android:text= "@{@generic_name(user.name)}"
  • android:text= "@{@string/generic_name,user.name}"

问题 3

何时会评估并运行监听器绑定表达式?

  • LiveData 保留的数据发生更改时
  • 当因配置更改而重新创建 activity 时
  • 发生 onClick() 等事件时
  • 当 activity 进入后台时

开始学习下一课:5.4:LiveData 转换

如需本课程中其他 Codelab 的链接,请参阅“Android Kotlin 基础知识”Codelab 着陆页