此 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
对象有何作用。- 如何将
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 应用作为起始代码,也可以下载起始应用。
- (可选)如果您未使用上一个 Codelab 中的代码,请下载此 Codelab 的起始代码。解压缩代码,然后在 Android Studio 中打开项目。
- 运行应用并玩游戏。
- 请注意,Skip 按钮会显示下一个单词并降低分数,而 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
对象)密切相关。在此任务中,您将向这些 LiveData
对象附加 Observer
对象。
- 在
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 返回确切对象之外的某些东西。在此任务中,您将为 GuessTheWord 应用中的 score
和 word
对象实现后备属性。
为得分和字词添加后备属性
- 在
GameViewModel
中,将当前score
对象设为private
。 - 为了遵循后备属性中使用的命名惯例,请将
score
更改为_score
。_score
属性现在是可在内部使用的游戏得分的可变版本。 - 创建一个名为
score
的LiveData
类型的公开版本。
// 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
。
当用户点按结束游戏按钮时,当前应用会转到得分屏幕。此外,还希望应用在玩家循环播放所有单词时导航到得分屏幕。在玩家说完最后一个单词后,您希望游戏自动结束,这样用户就无需点按该按钮了。
如需实现此功能,您需要在显示所有字词后,通过 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()
})
- 运行应用,玩游戏并仔细猜完所有单词。应用会自动转到得分屏幕,而不是留在游戏片段中,直到您点按结束游戏。
字词列表为空后,系统会设置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)
}
- 运行应用,玩游戏并仔细猜完所有单词。消息框下方短暂显示一条提示消息“游戏刚刚结束”的消息,这属于正常现象。
现在旋转设备或模拟器。消息框会再次显示!再旋转几次,您每次可能都会看到消息框。这是一个错误,因为在游戏结束时,消息框应仅显示一次。消息框不应在每次重新创建 Fragment 时都显示。您将在下一个任务中解决此问题。
第 2 步:重置游戏结束事件
通常,LiveData
仅在数据发生更改时才向观察者提供更新。此行为的一种例外情况是,观察者从非活跃状态更改为活跃状态时也会收到更新。
因此,系统会在您的应用中触发游戏结束消息框。在屏幕旋转后重新创建游戏 Fragment 时,它会从非活跃状态变为活跃状态。Fragment 中的观察者会重新连接到现有 ViewModel
并接收当前数据。系统会重新触发 gameFinished()
方法,并显示消息框。
在本任务中,您将通过重置 GameViewModel
中的 eventGameFinish
标志来修复此问题,并且仅显示一次消息框。
- 在
GameViewModel
中,添加onGameFinishComplete()
方法以重置游戏结束事件_eventGameFinish
。
/** Method for the game completed event **/
fun onGameFinishComplete() {
_eventGameFinish.value = false
}
- 在
GameFragment
中的gameFinished()
末尾,对viewModel
对象调用onGameFinishComplete()
。(暂时先在gameFinished()
中注释掉导航代码。)
private fun gameFinished() {
...
viewModel.onGameFinishComplete()
}
- 运行应用并玩游戏。浏览所有字词,然后更改设备的屏幕方向。消息框仅显示一次。
- 在
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。
在此任务中,您要将得分更改为 ScoreViewModel
中的 LiveData
对象,并向其附加一个观察器。此任务类似于将 LiveData
添加到 GameViewModel
时执行的操作。
您对 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
和一个观察器来更新得分。
在此任务中,您将向分数屏幕添加一个 Play Again 按钮,并使用 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() }
- 运行应用并玩游戏。游戏结束后,得分屏幕会显示最终得分和 Play Again 按钮。点按 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 着陆页。