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 对象。
  • 如何在 ViewModel 中使用 LiveData 存储数据。
  • 如何添加观察器方法来观察 LiveData 数据中的更改。

学习内容

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

您将执行的操作

  • GuessTheWord 布局中的视图使用界面控制器 (fragment) 来传达信息,从而间接与 ViewModel 对象通信。在此 Codelab 中,您将应用的视图绑定到 ViewModel 对象,以便视图与 ViewModel 对象进行直接通信。
  • 您将应用更改为使用 LiveData 作为数据绑定来源。进行此更改后,LiveData 对象会向界面通知数据中的更改,因此不再需要 LiveData 观察者方法。

在第 5 课的 Codelab 中,您将从起始代码开始开发 GuessTheWord 应用。GuessTheWord 是一款双人猜字谜游戏,玩家在游戏中可以协作以获得最高得分。

第一位玩家查看应用中的单词,然后依次表演每个单词,确保不让第二位玩家看到单词。第二位玩家尝试猜出该单词。

要玩这个游戏,第一个玩家需要在设备上打开应用,然后会看到一个字词,例如“吉他”,如下面的屏幕截图所示。

第一位玩家表演该字词,但要小心不要实际说出该字词。

  • 当第二位玩家正确猜出单词后,第一位玩家会按 Got It 按钮,这会将计数增加 1,并显示下一个单词。
  • 如果第二位玩家猜不出单词,第一位玩家可以按跳过按钮,这样一来,计数器会减 1,并跳到下一个单词。
  • 如需结束游戏,请按 End Game 按钮。(此功能未包含在本系列第一个 Codelab 的起始代码中。)

在此 Codelab 中,您将通过将数据绑定与 ViewModel 对象中的 LiveData 集成来改进 GuessTheWord 应用。这可自动实现布局中的视图与 ViewModel 对象之间的通信,并让您能够通过使用 LiveData 简化代码。

广告标题画面

游戏界面

得分界面

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

  1. (可选)如果您不使用上一个 Codelab 中的代码,请下载此 Codelab 的起始代码。解压缩代码,然后在 Android Studio 中打开项目。
  2. 运行应用并玩游戏。
  3. 请注意,点按 Got It 按钮后会显示下一个单词,并且得分会增加 1 分;而点按 Skip 按钮后会显示下一个单词,并且得分会减少 1 分。结束游戏按钮用于结束游戏。
  4. 循环浏览所有单词,并注意应用会自动导航到得分界面。

在之前的 Codelab 中,您使用数据绑定以类型安全的方式访问 GuessTheWord 应用中的视图。但数据绑定的真正强大之处在于,它可以按照名称所示,将数据直接绑定到应用中的视图对象。

当前应用架构

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

例如:

  • 知道了按钮在 game_fragment.xml 布局文件中定义为 Button 视图。
  • 当用户点按知道了按钮时,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. ScoreFragmentonCreateView() 内,初始化 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 步:向 game_fragment.xml 文件添加 word LiveData

在此步骤中,您要将当前单词文本视图直接绑定到 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. GameFragmentonCreateView() 中,初始化 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 步:将得分 LiveData 添加到 score_fragment.xml 文件

在此步骤中,您将 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 中的得分显示正确,且得分 fragment 中没有观察者。

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

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

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