此 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
的Boolean
MutableLiveData
对象。此对象将包含游戏结束事件。 - 初始化
_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
内使用private
MutableLiveData
,并在ViewModel
外返回LiveData
支持属性。
可观测的 LiveData
LiveData
遵循观察者模式。“可观测对象”是LiveData
对象,而观测者是界面控制器(如 fragment)中的方法。每当LiveData
封装的数据发生变化时,界面控制器中的观察者方法都会收到通知。- 如需使
LiveData
可观测,请使用observe()
方法将观察器对象附加到观察器(例如 activity 和 fragment)中的LiveData
引用。 - 此
LiveData
观察者模式可用于从ViewModel
向界面控制器进行通信。
Udacity 课程:
Android 开发者文档:
其他:
- Kotlin 中的后备属性
此部分列出了在由讲师主导的课程中,学生学习此 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
对象
开始学习下一课:
如需本课程中其他 Codelab 的链接,请参阅“Android Kotlin 基础知识”Codelab 着陆页。