此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。
简介
在上一个 Codelab 中,您在 GuessTheWord 应用中使用了 ViewModel,以使应用的数据在设备配置更改后继续留存。在此 Codelab 中,您将学习如何将 LiveData 与 ViewModel 类中的数据结合使用。LiveData 是 Android 架构组件之一,可让您构建在底层数据库发生更改时通知视图的数据对象。
如需使用 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 应用作为起始代码,也可以下载起始应用。
- (可选)如果您不使用上一个 Codelab 中的代码,请下载此 Codelab 的起始代码。解压缩代码,然后在 Android Studio 中打开项目。
- 运行应用并玩游戏。
- 请注意,点按 Skip 按钮后会显示下一个单词,并且得分会减 1;点按 Got It 按钮后会显示下一个单词,并且得分会加 1。结束游戏按钮用于结束游戏。
LiveData 是一种具有生命周期感知能力、可观察的数据存储器类。例如,您可以在 GuessTheWord 应用中将 LiveData 封装在当前得分周围。在此 Codelab 中,您将了解 LiveData 的几个特征:
LiveData是可观察的,这意味着当LiveData对象存储的数据发生更改时,观察器会收到通知。LiveData可存储数据;LiveData是一种可用于任何数据的封装容器。LiveData具有生命周期感知能力,也就是说它仅更新处于活跃生命周期状态(例如STARTED或RESUMED)的观察器。
在此任务中,您要将 GameViewModel 中的当前得分和当前单词数据转换为 LiveData,以此来了解如何将任何数据类型封装到 LiveData 对象中。在后续任务中,您将向这些 LiveData 对象添加观察器并了解如何观察 LiveData。
第 1 步:更改得分和单词以使用 LiveData
- 在
screens/game软件包下,打开GameViewModel文件。 - 将变量
score和word的类型更改为MutableLiveData。MutableLiveData是一个值可以更改的LiveData。MutableLiveData是一个泛型类,因此需要指定其存储的数据的类型。
// The current word
val word = MutableLiveData<String>()
// The current score
val score = MutableLiveData<Int>()- 在
GameViewModel中,在init代码块内,初始化score和word。如需更改LiveData变量的值,请对该变量使用setValue()方法。在 Kotlin 中,您可以使用value属性调用setValue()。
init {
word.value = ""
score.value = 0
...
}第 2 步:更新 LiveData 对象引用
score 和 word 变量现在属于 LiveData 类型。在此步骤中,您将使用 value 属性更改对这些变量的引用。
- 在
GameViewModel中,在onSkip()方法中,将score更改为score.value。请注意有关score可能为null的错误。您接下来将修复此错误。 - 如需解决此错误,请在
onSkip()中向score.value添加null检查。然后,对score调用minus()函数,该函数会执行null-安全减法。
fun onSkip() {
if (!wordList.isEmpty()) {
score.value = (score.value)?.minus(1)
}
nextWord()
}- 以相同的方式更新
onCorrect()方法:向score变量添加null检查,并使用plus()函数。
fun onCorrect() {
if (!wordList.isEmpty()) {
score.value = (score.value)?.plus(1)
}
nextWord()
}- 在
GameViewModel中的nextWord()方法内,将word引用更改为word.value。
private fun nextWord() {
if (!wordList.isEmpty()) {
//Select and remove a word from the list
word.value = wordList.removeAt(0)
}
}- 在
GameFragment中的updateWordText()方法内,将对viewModel.word的引用更改为viewModel.word.value.
/** Methods for updating the UI **/
private fun updateWordText() {
binding.wordText.text = viewModel.word.value
}- 在
GameFragment中的updateScoreText()方法内,将对viewModel.score的引用更改为viewModel.score.value.
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.value.toString()
}- 在
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)
}- 确保代码中没有错误。编译并运行应用。应用的功能应与之前相同。
此任务与上一个任务密切相关,在上一个任务中,您将得分和单词数据转换为 LiveData 对象。在此任务中,您将 Observer 对象附加到这些 LiveData 对象。
- 在
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。
- 当被观察的
LiveData对象存储的数据发生更改时,您刚刚创建的观察器会收到事件。在观察器内,使用新得分更新得分TextView。
/** Setting up LiveData observation relationship **/
viewModel.score.observe(this, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})- 将
Observer对象附加到当前字词LiveData对象。以您将Observer对象附加到当前得分的相同方式进行操作。
/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
binding.wordText.text = newWord
})当 score 或 word 的值发生变化时,屏幕上显示的 score 或 word 现在会自动更新。
- 在
GameFragment中,删除方法updateWordText()和updateScoreText()以及对它们的所有引用。您已不再需要它们,因为文本视图会通过LiveData观察器方法进行更新。 - 运行应用。游戏应用的运行应该和之前完全一样,但现在它使用的是
LiveData和LiveData观察器。
封装是一种限制对对象的某些字段进行直接访问的方法。封装对象时,您可以公开一组公开方法用于修改不公开的内部字段。通过使用封装,您可以控制其他类操纵这些内部字段的方式。
在当前代码中,任何外部类都可以使用 value 属性修改 score 和 word 变量,例如使用 viewModel.score.value。在您在此 Codelab 中开发的应用中,这可能并不重要,但在正式版应用中,您需要控制 ViewModel 对象中的数据。
只有 ViewModel 才能修改应用中的数据。但界面控制器需要读取数据,因此数据字段不能完全设为不公开。如需封装应用的数据,您需要同时使用 MutableLiveData 和 LiveData 对象。
MutableLiveData与LiveData的对比:
MutableLiveData对象中的数据可以更改,正如其名称所暗示的那样。在ViewModel之内,数据应可修改,因此使用MutableLiveData。LiveData对象中的数据可以读取,但无法更改。而在ViewModel之外,数据应可读取但无法修改,因此数据应作为LiveData公开。
若要执行此策略,您可以使用 Kotlin 后备属性。使用后备属性,可以从 getter 返回确切对象之外的某些其他内容。在此任务中,您将为“猜字游戏”应用中的 score 和 word 对象实现支持属性。
为得分和单词添加了后备属性
- 在
GameViewModel中,将当前score对象设为private。 - 为了遵循后备属性中使用的命名惯例,请将
score更改为_score。_score属性现在是游戏得分的可变版本,供内部使用。 - 创建
LiveData类型的公开版本,名为score。
// The current score
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>- 您看到初始化错误。发生此错误的原因是,在
GameFragment内,score是LiveData引用,而score无法再访问其 setter。如需详细了解 Kotlin 中的 getter 和 setter,请参阅 Getter 和 Setter。
如需解决此错误,请针对GameViewModel中的score对象替换get()方法,并返回后备属性_score。
val score: LiveData<Int>
get() = _score- 在
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)
}
...
}- 将
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 对象 word 和 score。
在当前应用中,当用户点按 End Game 按钮时,应用会导航到得分界面。您还希望应用在玩家猜完所有单词后跳转到得分界面。当玩家完成最后一个字词后,您希望游戏自动结束,这样用户就不必点按按钮。
如需实现此功能,您需要在显示完所有字词后,触发一个事件并将其从 ViewModel 传递给 fragment。为此,您可以使用 LiveData 观察者模式来模拟游戏结束事件。
观察者模式
观察者模式是一种软件设计模式。它指定了对象之间的通信:可观测对象(观测的“正文”)和观测者。可观测对象是一种会将其状态变化通知给观测者的对象。

对于此应用中的 LiveData,可观测对象(正文)是 LiveData 对象,而观测者是界面控制器(例如 fragment)中的方法。当 LiveData 中封装的数据发生变化时,就会发生状态变化。LiveData 类对于从 ViewModel 向 fragment 进行通信至关重要。
第 1 步:使用 LiveData 检测游戏结束事件
在此任务中,您将使用 LiveData 观察者模式来模拟游戏结束事件。
- 在
GameViewModel中,创建一个名为_eventGameFinish的BooleanMutableLiveData对象。此对象将包含游戏结束事件。 - 初始化
_eventGameFinish对象后,创建并初始化一个名为eventGameFinish的后备属性。
// Event which triggers the end of the game
private val _eventGameFinish = MutableLiveData<Boolean>()
val eventGameFinish: LiveData<Boolean>
get() = _eventGameFinish- 在
GameViewModel中,添加onGameFinish()方法。在该方法中,将游戏结束事件eventGameFinish设置为true。
/** Method for the game completed event **/
fun onGameFinish() {
_eventGameFinish.value = true
}- 在
GameViewModel中,在nextWord()方法内,如果字词列表为空,则结束游戏。
private fun nextWord() {
if (wordList.isEmpty()) {
onGameFinish()
} else {
//Select and remove a _word from the list
_word.value = wordList.removeAt(0)
}
}- 在
GameFragment中的onCreateView()内,在初始化viewModel后,为eventGameFinish附加一个观察器。使用observe()方法。在 lambda 函数内,调用gameFinished()方法。
// Observer for the Game finished event
viewModel.eventGameFinish.observe(this, Observer<Boolean> { hasFinished ->
if (hasFinished) gameFinished()
})- 运行应用,玩游戏,猜完所有单词。应用会自动导航到得分界面,而不是停留在游戏 fragment 中,直到您点按结束游戏。
在字词列表为空后,系统会设置eventGameFinish,调用游戏 fragment 中的关联观察者方法,然后应用会导航到屏幕 fragment。 - 您添加的代码引入了生命周期问题。为了解此问题,请在
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)
}
- 运行应用,玩游戏,猜完所有单词。游戏屏幕底部会短暂显示一条消息框消息,提示“游戏刚刚结束”,这是预期行为。
现在,旋转设备或模拟器。消息框会再次显示!再旋转几次设备,您可能会每次都看到该 Toast。这是一个 bug,因为只有在游戏结束时,Toast 才应显示一次。不应在每次重新创建 fragment 时都显示 Toast。您将在下一项任务中解决此问题。
|
|
第 2 步:重置游戏完成事件
通常,LiveData 仅在数据发生更改时才向观察者发送更新。此行为的一种例外情况是,观察者从非活跃状态更改为活跃状态时也会收到更新。
这就是为什么您的应用中会反复触发游戏结束 Toast 的原因。当游戏 fragment 在屏幕旋转后重新创建时,它会从非活跃状态变为活跃状态。fragment 中的观测器重新连接到现有的 ViewModel 并接收当前数据。系统会重新触发 gameFinished() 方法,并显示 Toast。
在此任务中,您将通过在 GameViewModel 中重置 eventGameFinish 标志来解决此问题并仅显示一次 Toast。
- 在
GameViewModel中,添加一个onGameFinishComplete()方法来重置游戏结束事件_eventGameFinish。
/** Method for the game completed event **/
fun onGameFinishComplete() {
_eventGameFinish.value = false
}- 在
GameFragment中的gameFinished()末尾,对viewModel对象调用onGameFinishComplete()。(暂时将gameFinished()中的导航代码注释掉。)
private fun gameFinished() {
...
viewModel.onGameFinishComplete()
}- 运行应用并玩游戏。浏览所有字词,然后更改设备的屏幕方向。Toast 仅显示一次。
- 在
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。
- 运行应用并玩游戏。确保应用在您完成所有单词后自动导航到最终得分界面。
|
|
太棒了!您的应用使用 LiveData 触发游戏结束事件,以从 GameViewModel 向游戏 fragment 传达字词列表为空的消息。然后,游戏 fragment 会导航到得分 fragment。
在此任务中,您将 ScoreViewModel 中的得分更改为 LiveData 对象,并为其附加一个观察器。此任务与您之前向 GameViewModel 添加 LiveData 时执行的操作类似。
您对 ScoreViewModel 进行这些更改是为了完整性,以便应用中的所有数据都使用 LiveData。
- 在
ScoreViewModel中,将score变量类型更改为MutableLiveData。按照惯例将其重命名为_score,并添加后备属性。
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
get() = _score- 在
ScoreViewModel中,在init代码块内,初始化_score。您可以根据需要移除或保留init块中的日志。
init {
_score.value = finalScore
}- 在
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。
- 运行应用并玩游戏。应用应像之前一样运行,但现在它使用
LiveData和观察器来更新得分。
在此任务中,您将向得分界面添加一个再玩一次按钮,并使用 LiveData 事件实现其点击监听器。该按钮会触发一个事件,以从得分界面导航到游戏界面。
应用的起始代码包含再玩一次按钮,但该按钮处于隐藏状态。
- 在
res/layout/score_fragment.xml中,对于play_again_button按钮,将visibility属性的值更改为visible。
<Button
android:id="@+id/play_again_button"
...
android:visibility="visible"
/>- 在
ScoreViewModel中,添加一个LiveData对象来保存名为_eventPlayAgain的Boolean。此对象用于保存LiveData事件,以便从得分界面导航到游戏界面。
private val _eventPlayAgain = MutableLiveData<Boolean>()
val eventPlayAgain: LiveData<Boolean>
get() = _eventPlayAgain- 在
ScoreViewModel中,定义用于设置和重置事件的方法_eventPlayAgain。
fun onPlayAgain() {
_eventPlayAgain.value = true
}
fun onPlayAgainComplete() {
_eventPlayAgain.value = false
}- 在
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。
- 在
ScoreFragment的onCreateView()中,为 PlayAgain 按钮添加点击监听器,并调用viewModel.onPlayAgain()。
binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() }- 运行应用并玩游戏。游戏结束后,得分界面会显示最终得分和再玩一次按钮。点按 PlayAgain 按钮,应用会导航到游戏界面,以便您再次玩游戏。

做得好!您更改了应用的架构,以在 ViewModel 中使用 LiveData 对象,并为 LiveData 对象附加了观察者。当 LiveData 存储的值发生变化时,LiveData 会通知观察器对象。
Android Studio 项目:GuessTheWord
LiveData
LiveData是一种具有生命周期感知能力、可观察的数据存储器类,属于 Android 架构组件之一。- 您可以使用
LiveData,以便在数据更新时自动更新界面。 LiveData是可观测的,这意味着当LiveData对象存储的数据发生更改时,activity 或 fragment 等观测器会收到通知。LiveData可存储数据;它是一种可用于任何数据的封装容器。LiveData具有生命周期感知能力,也就是说它仅更新处于活跃生命周期状态(例如STARTED或RESUMED)的观察器。
添加 LiveData
- 将
ViewModel中数据变量的类型更改为LiveData或MutableLiveData。
MutableLiveData 是一个值可以更改的 LiveData 对象。MutableLiveData 是一个泛型类,因此需要指定其存储的数据的类型。
- 如需更改
LiveData所持有的数据的值,请对LiveData变量使用setValue()方法。
封装 LiveData
ViewModel内的LiveData应可修改。在ViewModel之外,LiveData应该是可读的。可以使用 Kotlin 后备属性来实现此目的。- 使用 Kotlin 后备属性,可以从 getter 返回确切对象之外的某些其他内容。
- 如需封装
LiveData,请在ViewModel内使用privateMutableLiveData,并在ViewModel外返回LiveData支持属性。
可观测的 LiveData
LiveData遵循观察者模式。“可观测对象”是LiveData对象,而观测者是界面控制器(如 fragment)中的方法。每当LiveData封装的数据发生变化时,界面控制器中的观察者方法都会收到通知。- 如需使
LiveData可观测,请使用observe()方法将观察器对象附加到观察器(例如 activity 和 fragment)中的LiveData引用。 - 此
LiveData观察者模式可用于从ViewModel向界面控制器进行通信。
Udacity 课程:
Android 开发者文档:
其他:
- Kotlin 中的后备属性
此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:
- 根据需要布置作业。
- 告知学生如何提交家庭作业。
- 给家庭作业评分。
讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。
如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。
回答以下问题
问题 1
如何封装 ViewModel 中存储的 LiveData,才能使外部对象能够读取数据而无法更新数据?
- 在
ViewModel对象内,将该数据的数据类型更改为privateLiveData。使用后备属性公开MutableLiveData类型的只读数据。 - 在
ViewModel对象内,将该数据的数据类型更改为privateMutableLiveData。使用后备属性公开LiveData类型的只读数据。 - 在界面控制器内,将该数据的数据类型更改为
privateMutableLiveData。使用后备属性公开LiveData类型的只读数据。 - 在
ViewModel对象内,将该数据的数据类型更改为LiveData。使用后备属性公开LiveData类型的只读数据。
问题 2
界面控制器处于以下哪一种状态时,LiveData 会更新界面控制器(例如 fragment)?
- 已恢复
- 在后台
- 已暂停
- 已停止
问题 3
在 LiveData 观察者模式下,可观察项(被观察对象)是什么?
- observer 方法
LiveData对象中的数据- 界面控制器
ViewModel对象
开始学习下一课:
如需本课程中其他 Codelab 的链接,请参阅“Android Kotlin 基础知识”Codelab 着陆页。




