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 对象有何作用。
  • 如何将 LiveData 添加到 ViewModel 中存储的数据。
  • 何时以及如何使用 MutableLiveData
  • 如何添加观察器方法来观察 LiveData. 中的更改?
  • 如何使用后备属性封装 LiveData
  • 如何在界面控制器与其对应的 ViewModel 之间进行通信。

您将执行的操作

  • 在 GuessTheWord 应用中使用 LiveData 作为单词和分数。
  • 添加观察器,观察单词或分数何时发生变化。
  • 更新显示已更改值的文本视图。
  • 使用 LiveData 观察器模式添加游戏结束事件。
  • 实现 Play Again 按钮。

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

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

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

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

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

在此 Codelab 中,您将改进 GuessTheWord 应用,方法是在用户循环遍历应用中的所有字词时,结束游戏。此外,您还可以在得分 Fragment 中添加 Play Again 按钮,以便用户能够再次玩游戏。

广告标题画面

游戏屏幕

分数屏幕

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

  1. (可选)如果您未使用上一个 Codelab 中的代码,请下载此 Codelab 的起始代码。解压缩代码,然后在 Android Studio 中打开项目。
  2. 运行应用并玩游戏。
  3. 请注意,Skip 按钮会显示下一个单词并降低分数,而 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. GameViewModelonSkip() 方法中,将 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 对象)密切相关。在此任务中,您将向这些 LiveData 对象附加 Observer 对象。

  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 返回确切对象之外的某些东西。在此任务中,您将为 GuessTheWord 应用中的 scoreword 对象实现后备属性。

为得分和字词添加后备属性

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

    要解决该错误,请替换 GameViewModelscore 对象的 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

当用户点按结束游戏按钮时,当前应用会转到得分屏幕。此外,还希望应用在玩家循环播放所有单词时导航到得分屏幕。在玩家说完最后一个单词后,您希望游戏自动结束,这样用户就无需点按该按钮了。

如需实现此功能,您需要在显示所有字词后,通过 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. 运行应用,玩游戏并仔细猜完所有单词。应用会自动转到得分屏幕,而不是留在游戏片段中,直到您点按结束游戏

    字词列表为空后,系统会设置 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. 运行应用,玩游戏并仔细猜完所有单词。消息框下方短暂显示一条提示消息“游戏刚刚结束”的消息,这属于正常现象。

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

第 2 步:重置游戏结束事件

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

因此,系统会在您的应用中触发游戏结束消息框。在屏幕旋转后重新创建游戏 Fragment 时,它会从非活跃状态变为活跃状态。Fragment 中的观察者会重新连接到现有 ViewModel 并接收当前数据。系统会重新触发 gameFinished() 方法,并显示消息框。

在本任务中,您将通过重置 GameViewModel 中的 eventGameFinish 标志来修复此问题,并且仅显示一次消息框。

  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. 运行应用并玩游戏。浏览所有字词,然后更改设备的屏幕方向。消息框仅显示一次。
  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。

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

您对 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 和一个观察器来更新得分。

在此任务中,您将向分数屏幕添加一个 Play Again 按钮,并使用 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. ScoreFragment 中的 onCreateView() 内,为 PlayAgain 按钮添加点击监听器并调用 viewModel.onPlayAgain()
binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. 运行应用并玩游戏。游戏结束后,得分屏幕会显示最终得分和 Play Again 按钮。点按 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 着陆页