Android Kotlin 基础知识 05.2:LiveData 和 LiveData 观察器

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

简介

在上一个 Codelab 中,您在 GuessTheWord 应用中使用了 ViewModel,以使应用的数据在设备配置更改后继续留存。在此 Codelab 中,您将学习如何将 LiveDataViewModel 类中的数据结合使用。LiveDataAndroid 架构组件之一,可让您构建在底层数据库发生更改时通知视图的数据对象。

如需使用 LiveData 类,您需要设置“观察器”(例如 activity 或 fragment),用于观察应用数据的变化。LiveData 具有生命周期感知能力,因此它仅更新处于活跃生命周期状态的应用组件观察器。

您应当已掌握的内容

  • 如何使用 Kotlin 创建基本 Android 应用。
  • 如何在应用的目的地之间导航。
  • activity 和 fragment 生命周期。
  • 如何在应用中使用 ViewModel 对象。
  • 如何使用 ViewModelProvider.Factory 接口创建 ViewModel 对象。

学习内容

  • LiveData 对象的实用之处。
  • 如何向存储在 ViewModel 中的数据添加 LiveData
  • 何时以及如何使用 MutableLiveData
  • 如何添加观察器方法来观察 LiveData. 中的更改?
  • 如何使用后备属性封装 LiveData
  • 如何实现界面控制器与其对应的 ViewModel 之间的通信。

您将执行的操作

  • 在 GuessTheWord 应用中,将 LiveData 用于单词和得分。
  • 添加观察器,以便在字词或得分发生变化时收到通知。
  • 更新显示已更改值的文本视图。
  • 使用 LiveData 观察者模式添加游戏结束事件。
  • 实现再次播放按钮。

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

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

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

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

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

在此 Codelab 中,您将改进 GuessTheWord 应用,添加一个事件,以便在用户轮流猜完应用中的所有单词后结束游戏。您还将在得分 fragment 中添加一个再玩一次 按钮,以便用户可以再次玩游戏。

广告标题画面

游戏界面

得分界面

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

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

LiveData 是一种具有生命周期感知能力、可观察的数据存储器类。例如,您可以在 GuessTheWord 应用中将 LiveData 封装在当前得分周围。在此 Codelab 中,您将了解 LiveData 的几个特征:

  • LiveData 是可观察的,这意味着当 LiveData 对象存储的数据发生更改时,观察器会收到通知。
  • LiveData 可存储数据;LiveData 是一种可用于任何数据的封装容器。
  • LiveData 具有生命周期感知能力,也就是说它仅更新处于活跃生命周期状态(例如 STARTEDRESUMED)的观察器。

在此任务中,您要将 GameViewModel 中的当前得分和当前单词数据转换为 LiveData,以此来了解如何将任何数据类型封装到 LiveData 对象中。在后续任务中,您将向这些 LiveData 对象添加观察器并了解如何观察 LiveData

第 1 步:更改得分和单词以使用 LiveData

  1. screens/game 软件包下,打开 GameViewModel 文件。
  2. 将变量 scoreword 的类型更改为 MutableLiveData

    MutableLiveData 是一个值可以更改的 LiveDataMutableLiveData 是一个泛型类,因此需要指定其存储的数据的类型。
// The current word
val word = MutableLiveData<String>()
// The current score
val score = MutableLiveData<Int>()
  1. GameViewModel 中,在 init 代码块内,初始化 scoreword。如需更改 LiveData 变量的值,请对该变量使用 setValue() 方法。在 Kotlin 中,您可以使用 value 属性调用 setValue()
init {

   word.value = ""
   score.value = 0
  ...
}

第 2 步:更新 LiveData 对象引用

scoreword 变量现在属于 LiveData 类型。在此步骤中,您将使用 value 属性更改对这些变量的引用。

  1. GameViewModel 中,在 onSkip() 方法中,将 score 更改为 score.value。请注意有关 score 可能为 null 的错误。您接下来将修复此错误。
  2. 如需解决此错误,请在 onSkip() 中向 score.value 添加 null 检查。然后,对 score 调用 minus() 函数,该函数会执行 null-安全减法。
fun onSkip() {
   if (!wordList.isEmpty()) {
       score.value = (score.value)?.minus(1)
   }
   nextWord()
}
  1. 以相同的方式更新 onCorrect() 方法:向 score 变量添加 null 检查,并使用 plus() 函数。
fun onCorrect() {
   if (!wordList.isEmpty()) {
       score.value = (score.value)?.plus(1)
   }
   nextWord()
}
  1. GameViewModel 中的 nextWord() 方法内,将 word 引用更改为 word.value
private fun nextWord() {
   if (!wordList.isEmpty()) {
       //Select and remove a word from the list
       word.value = wordList.removeAt(0)
   }
}
  1. GameFragment 中的 updateWordText() 方法内,将对 viewModel.word 的引用更改为 viewModel.word.value.
/** Methods for updating the UI **/
private fun updateWordText() {
   binding.wordText.text = viewModel.word.value
}
  1. GameFragment 中的 updateScoreText() 方法内,将对 viewModel.score 的引用更改为 viewModel.score.value.
private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.value.toString()
}
  1. GameFragment 中的 gameFinished() 方法内,将对 viewModel.score 的引用更改为 viewModel.score.value。添加必需的 null 安全检查。
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score.value?:0
   NavHostFragment.findNavController(this).navigate(action)
}
  1. 确保代码中没有错误。编译并运行应用。应用的功能应与之前相同。

此任务与上一个任务密切相关,在上一个任务中,您将得分和单词数据转换为 LiveData 对象。在此任务中,您将 Observer 对象附加到这些 LiveData 对象。

  1. GameFragment, 中,于 onCreateView() 方法内,将 Observer 对象附加到当前得分的 LiveData 对象,即 viewModel.score。使用 observe() 方法,并将代码放在 viewModel 的初始化之后。使用 lambda 表达式简化代码。(lambda 表达式是一种未声明但会立即以表达式形式传递的匿名函数。)
viewModel.score.observe(this, Observer { newScore ->
})

解析对 Observer 的引用。为此,请点击 Observer,按 Alt+Enter(在 Mac 上,按 Option+Enter),然后导入 androidx.lifecycle.Observer

  1. 当被观察的 LiveData 对象存储的数据发生更改时,您刚刚创建的观察器会收到事件。在观察器内,使用新得分更新得分 TextView
/** Setting up LiveData observation relationship **/
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. Observer 对象附加到当前字词 LiveData 对象。以您将 Observer 对象附加到当前得分的相同方式进行操作。
/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
   binding.wordText.text = newWord
})

scoreword 的值发生变化时,屏幕上显示的 scoreword 现在会自动更新。

  1. GameFragment 中,删除方法 updateWordText()updateScoreText() 以及对它们的所有引用。您已不再需要它们,因为文本视图会通过 LiveData 观察器方法进行更新。
  2. 运行应用。游戏应用的运行应该和之前完全一样,但现在它使用的是 LiveDataLiveData 观察器。

封装是一种限制对对象的某些字段进行直接访问的方法。封装对象时,您可以公开一组公开方法用于修改不公开的内部字段。通过使用封装,您可以控制其他类操纵这些内部字段的方式。

在当前代码中,任何外部类都可以使用 value 属性修改 scoreword 变量,例如使用 viewModel.score.value。在您在此 Codelab 中开发的应用中,这可能并不重要,但在正式版应用中,您需要控制 ViewModel 对象中的数据。

只有 ViewModel 才能修改应用中的数据。但界面控制器需要读取数据,因此数据字段不能完全设为不公开。如需封装应用的数据,您需要同时使用 MutableLiveDataLiveData 对象。

MutableLiveDataLiveData的对比:

  • MutableLiveData 对象中的数据可以更改,正如其名称所暗示的那样。在 ViewModel 之内,数据应可修改,因此使用 MutableLiveData
  • LiveData 对象中的数据可以读取,但无法更改。而在 ViewModel 之外,数据应可读取但无法修改,因此数据应作为 LiveData 公开。

若要执行此策略,您可以使用 Kotlin 后备属性。使用后备属性,可以从 getter 返回确切对象之外的某些其他内容。在此任务中,您将为“猜字游戏”应用中的 scoreword 对象实现支持属性。

为得分和单词添加了后备属性

  1. GameViewModel 中,将当前 score 对象设为 private
  2. 为了遵循后备属性中使用的命名惯例,请将 score 更改为 _score_score 属性现在是游戏得分的可变版本,供内部使用。
  3. 创建 LiveData 类型的公开版本,名为 score
// The current score
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
  1. 您看到初始化错误。发生此错误的原因是,在 GameFragment 内,scoreLiveData 引用,而 score 无法再访问其 setter。如需详细了解 Kotlin 中的 getter 和 setter,请参阅 Getter 和 Setter

    如需解决此错误,请针对 GameViewModel 中的 score 对象替换 get() 方法,并返回后备属性 _score
val score: LiveData<Int>
   get() = _score
  1. GameViewModel 中,将 score 的引用更改为其内部可变版本 _score
init {
   ...
   _score.value = 0
   ...
}

...
fun onSkip() {
   if (!wordList.isEmpty()) {
       _score.value = (score.value)?.minus(1)
   }
  ...
}

fun onCorrect() {
   if (!wordList.isEmpty()) {
       _score.value = (score.value)?.plus(1)
   }
   ...
}
  1. word 对象重命名为 _word,并为其添加一个后备属性,就像您为 score 对象所做的那样。
// The current word
private val _word = MutableLiveData<String>()
val word: LiveData<String>
   get() = _word
...
init {
   _word.value = ""
   ...
}
...
private fun nextWord() {
   if (!wordList.isEmpty()) {
       //Select and remove a word from the list
       _word.value = wordList.removeAt(0)
   }
}

太棒了,您已封装 LiveData 对象 wordscore

在当前应用中,当用户点按 End Game 按钮时,应用会导航到得分界面。您还希望应用在玩家猜完所有单词后跳转到得分界面。当玩家完成最后一个字词后,您希望游戏自动结束,这样用户就不必点按按钮。

如需实现此功能,您需要在显示完所有字词后,触发一个事件并将其从 ViewModel 传递给 fragment。为此,您可以使用 LiveData 观察者模式来模拟游戏结束事件。

观察者模式

观察者模式是一种软件设计模式。它指定了对象之间的通信:可观测对象(观测的“正文”)和观测者。可观测对象是一种会将其状态变化通知给观测者的对象。

对于此应用中的 LiveData,可观测对象(正文)是 LiveData 对象,而观测者是界面控制器(例如 fragment)中的方法。当 LiveData 中封装的数据发生变化时,就会发生状态变化。LiveData 类对于从 ViewModel 向 fragment 进行通信至关重要。

第 1 步:使用 LiveData 检测游戏结束事件

在此任务中,您将使用 LiveData 观察者模式来模拟游戏结束事件。

  1. GameViewModel 中,创建一个名为 _eventGameFinishBoolean MutableLiveData 对象。此对象将包含游戏结束事件。
  2. 初始化 _eventGameFinish 对象后,创建并初始化一个名为 eventGameFinish 的后备属性。
// Event which triggers the end of the game
private val _eventGameFinish = MutableLiveData<Boolean>()
val eventGameFinish: LiveData<Boolean>
   get() = _eventGameFinish
  1. GameViewModel 中,添加 onGameFinish() 方法。在该方法中,将游戏结束事件 eventGameFinish 设置为 true
/** Method for the game completed event **/
fun onGameFinish() {
   _eventGameFinish.value = true
}
  1. GameViewModel 中,在 nextWord() 方法内,如果字词列表为空,则结束游戏。
private fun nextWord() {
   if (wordList.isEmpty()) {
       onGameFinish()
   } else {
       //Select and remove a _word from the list
       _word.value = wordList.removeAt(0)
   }
}
  1. GameFragment 中的 onCreateView() 内,在初始化 viewModel 后,为 eventGameFinish 附加一个观察器。使用 observe() 方法。在 lambda 函数内,调用 gameFinished() 方法。
// Observer for the Game finished event
viewModel.eventGameFinish.observe(this, Observer<Boolean> { hasFinished ->
   if (hasFinished) gameFinished()
})
  1. 运行应用,玩游戏,猜完所有单词。应用会自动导航到得分界面,而不是停留在游戏 fragment 中,直到您点按结束游戏

    在字词列表为空后,系统会设置 eventGameFinish,调用游戏 fragment 中的关联观察者方法,然后应用会导航到屏幕 fragment。
  2. 您添加的代码引入了生命周期问题。为了解此问题,请在 GameFragment 类中,注释掉 gameFinished() 方法中的导航代码。请务必在方法中保留 Toast 消息。
private fun gameFinished() {
       Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
//        val action = GameFragmentDirections.actionGameToScore()
//        action.score = viewModel.score.value?:0
//        NavHostFragment.findNavController(this).navigate(action)
   }
  1. 运行应用,玩游戏,猜完所有单词。游戏屏幕底部会短暂显示一条消息框消息,提示“游戏刚刚结束”,这是预期行为。

现在,旋转设备或模拟器。消息框会再次显示!再旋转几次设备,您可能会每次都看到该 Toast。这是一个 bug,因为只有在游戏结束时,Toast 才应显示一次。不应在每次重新创建 fragment 时都显示 Toast。您将在下一项任务中解决此问题。

第 2 步:重置游戏完成事件

通常,LiveData 仅在数据发生更改时才向观察者发送更新。此行为的一种例外情况是,观察者从非活跃状态更改为活跃状态时也会收到更新。

这就是为什么您的应用中会反复触发游戏结束 Toast 的原因。当游戏 fragment 在屏幕旋转后重新创建时,它会从非活跃状态变为活跃状态。fragment 中的观测器重新连接到现有的 ViewModel 并接收当前数据。系统会重新触发 gameFinished() 方法,并显示 Toast。

在此任务中,您将通过在 GameViewModel 中重置 eventGameFinish 标志来解决此问题并仅显示一次 Toast。

  1. GameViewModel 中,添加一个 onGameFinishComplete() 方法来重置游戏结束事件 _eventGameFinish
/** Method for the game completed event **/

fun onGameFinishComplete() {
   _eventGameFinish.value = false
}
  1. GameFragment 中的 gameFinished() 末尾,对 viewModel 对象调用 onGameFinishComplete()。(暂时将 gameFinished() 中的导航代码注释掉。)
private fun gameFinished() {
   ...
   viewModel.onGameFinishComplete()
}
  1. 运行应用并玩游戏。浏览所有字词,然后更改设备的屏幕方向。Toast 仅显示一次。
  2. GameFragment 中,在 gameFinished() 方法内,取消对导航代码的注释。

    如需在 Android Studio 中取消注释,请选择已注释掉的行,然后按 Control+/(在 Mac 上按 Command+/)。
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score.value?:0
   findNavController(this).navigate(action)
   viewModel.onGameFinishComplete()
}

如果 Android Studio 提示,请导入 androidx.navigation.fragment.NavHostFragment.findNavController

  1. 运行应用并玩游戏。确保应用在您完成所有单词后自动导航到最终得分界面。

太棒了!您的应用使用 LiveData 触发游戏结束事件,以从 GameViewModel 向游戏 fragment 传达字词列表为空的消息。然后,游戏 fragment 会导航到得分 fragment。

在此任务中,您将 ScoreViewModel 中的得分更改为 LiveData 对象,并为其附加一个观察器。此任务与您之前向 GameViewModel 添加 LiveData 时执行的操作类似。

您对 ScoreViewModel 进行这些更改是为了完整性,以便应用中的所有数据都使用 LiveData

  1. ScoreViewModel 中,将 score 变量类型更改为 MutableLiveData。按照惯例将其重命名为 _score,并添加后备属性。
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
   get() = _score
  1. ScoreViewModel 中,在 init 代码块内,初始化 _score。您可以根据需要移除或保留 init 块中的日志。
init {
   _score.value = finalScore
}
  1. ScoreFragment 中的 onCreateView() 内,初始化 viewModel 后,为得分 LiveData 对象附加一个观察器。在 lambda 表达式内,将得分值设置为得分文本视图。从 ViewModel 中移除直接为文本视图分配得分值的代码。

要添加的代码:

// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})

要移除的代码:

binding.scoreText.text = viewModel.score.toString()

当 Android Studio 提示时,导入 androidx.lifecycle.Observer

  1. 运行应用并玩游戏。应用应像之前一样运行,但现在它使用 LiveData 和观察器来更新得分。

在此任务中,您将向得分界面添加一个再玩一次按钮,并使用 LiveData 事件实现其点击监听器。该按钮会触发一个事件,以从得分界面导航到游戏界面。

应用的起始代码包含再玩一次按钮,但该按钮处于隐藏状态。

  1. res/layout/score_fragment.xml 中,对于 play_again_button 按钮,将 visibility 属性的值更改为 visible
<Button
   android:id="@+id/play_again_button"
...
   android:visibility="visible"
 />
  1. ScoreViewModel 中,添加一个 LiveData 对象来保存名为 _eventPlayAgainBoolean。此对象用于保存 LiveData 事件,以便从得分界面导航到游戏界面。
private val _eventPlayAgain = MutableLiveData<Boolean>()
val eventPlayAgain: LiveData<Boolean>
   get() = _eventPlayAgain
  1. ScoreViewModel 中,定义用于设置和重置事件的方法 _eventPlayAgain
fun onPlayAgain() {
   _eventPlayAgain.value = true
}
fun onPlayAgainComplete() {
   _eventPlayAgain.value = false
}
  1. ScoreFragment 中,为 eventPlayAgain 添加一个观察器。将代码放在 onCreateView() 的末尾,在 return 语句之前。在 lambda 表达式中,返回到游戏界面并重置 eventPlayAgain
// Navigates back to game when button is pressed
viewModel.eventPlayAgain.observe(this, Observer { playAgain ->
   if (playAgain) {
      findNavController().navigate(ScoreFragmentDirections.actionRestart())
       viewModel.onPlayAgainComplete()
   }
})

当 Android Studio 提示时,导入 androidx.navigation.fragment.findNavController

  1. ScoreFragmentonCreateView() 中,为 PlayAgain 按钮添加点击监听器,并调用 viewModel.onPlayAgain()
binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. 运行应用并玩游戏。游戏结束后,得分界面会显示最终得分和再玩一次按钮。点按 PlayAgain 按钮,应用会导航到游戏界面,以便您再次玩游戏。

做得好!您更改了应用的架构,以在 ViewModel 中使用 LiveData 对象,并为 LiveData 对象附加了观察者。当 LiveData 存储的值发生变化时,LiveData 会通知观察器对象。

Android Studio 项目:GuessTheWord

LiveData

  • LiveData 是一种具有生命周期感知能力、可观察的数据存储器类,属于 Android 架构组件之一。
  • 您可以使用 LiveData,以便在数据更新时自动更新界面。
  • LiveData 是可观测的,这意味着当 LiveData 对象存储的数据发生更改时,activity 或 fragment 等观测器会收到通知。
  • LiveData 可存储数据;它是一种可用于任何数据的封装容器。
  • LiveData 具有生命周期感知能力,也就是说它仅更新处于活跃生命周期状态(例如 STARTEDRESUMED)的观察器。

添加 LiveData

  • ViewModel 中数据变量的类型更改为 LiveDataMutableLiveData

MutableLiveData 是一个值可以更改的 LiveData 对象。MutableLiveData 是一个泛型类,因此需要指定其存储的数据的类型。

  • 如需更改 LiveData 所持有的数据的值,请对 LiveData 变量使用 setValue() 方法。

封装 LiveData

  • ViewModel 内的 LiveData 应可修改。在 ViewModel 之外,LiveData 应该是可读的。可以使用 Kotlin 后备属性来实现此目的。
  • 使用 Kotlin 后备属性,可以从 getter 返回确切对象之外的某些其他内容。
  • 如需封装 LiveData,请在 ViewModel 内使用 private MutableLiveData,并在 ViewModel 外返回 LiveData 支持属性。

可观测的 LiveData

  • LiveData 遵循观察者模式。“可观测对象”是 LiveData 对象,而观测者是界面控制器(如 fragment)中的方法。每当 LiveData 封装的数据发生变化时,界面控制器中的观察者方法都会收到通知。
  • 如需使 LiveData 可观测,请使用 observe() 方法将观察器对象附加到观察器(例如 activity 和 fragment)中的 LiveData 引用。
  • LiveData 观察者模式可用于从 ViewModel 向界面控制器进行通信。

Udacity 课程:

Android 开发者文档:

其他:

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

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

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

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

回答以下问题

问题 1

如何封装 ViewModel 中存储的 LiveData,才能使外部对象能够读取数据而无法更新数据?

  • ViewModel 对象内,将该数据的数据类型更改为 private LiveData。使用后备属性公开 MutableLiveData 类型的只读数据。
  • ViewModel 对象内,将该数据的数据类型更改为 private MutableLiveData。使用后备属性公开 LiveData 类型的只读数据。
  • 在界面控制器内,将该数据的数据类型更改为 private MutableLiveData。使用后备属性公开 LiveData 类型的只读数据。
  • ViewModel 对象内,将该数据的数据类型更改为 LiveData。使用后备属性公开 LiveData 类型的只读数据。

问题 2

界面控制器处于以下哪一种状态时,LiveData 会更新界面控制器(例如 fragment)?

  • 已恢复
  • 在后台
  • 已暂停
  • 已停止

问题 3

LiveData 观察者模式下,可观察项(被观察对象)是什么?

  • observer 方法
  • LiveData 对象中的数据
  • 界面控制器
  • ViewModel 对象

开始学习下一课:5.3:将 ViewModel 和 LiveData 与数据绑定集成

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