此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。
简介
在本课程的之前几个 Codelab 中,您改进了 GuessTheWord 应用的代码。该应用现在使用 ViewModel
对象,因此应用数据在设备配置更改(例如屏幕旋转和键盘可用性更改)后仍会保留。您还添加了可观测的 LiveData
,以便在观测到的数据发生更改时自动通知视图。
在此 Codelab 中,您将继续使用 GuessTheWord 应用。您会将视图绑定到应用中的 ViewModel
类,以便布局中的视图直接与 ViewModel
对象通信。(在您的应用中,到目前为止,视图一直通过应用的 fragment 与 ViewModel
进行间接通信。)将数据绑定与 ViewModel
对象集成后,您不再需要在应用 fragment 中使用点击处理程序,因此将其移除。
您还将更改 GuessTheWord 应用,使其使用 LiveData
作为数据绑定来源,以便在不使用 LiveData
观察器方法的情况下将数据变化通知给界面。
您应当已掌握的内容
- 如何使用 Kotlin 创建基本 Android 应用。
- activity 和 fragment 生命周期如何运作。
- 如何在应用中使用
ViewModel
对象。 - 如何在
ViewModel
中使用LiveData
存储数据。 - 如何添加观察器方法来观察
LiveData
数据中的更改。
学习内容
- 如何使用数据绑定库的元素。
- 如何将
ViewModel
与数据绑定集成。 - 如何将
LiveData
与数据绑定集成。 - 如何使用监听器绑定来替换 fragment 中的点击监听器。
- 如何向数据绑定表达式添加字符串格式设置。
您将执行的操作
- GuessTheWord 布局中的视图使用界面控制器 (fragment) 来传达信息,从而间接与
ViewModel
对象通信。在此 Codelab 中,您将应用的视图绑定到ViewModel
对象,以便视图与ViewModel
对象进行直接通信。 - 您将应用更改为使用
LiveData
作为数据绑定来源。进行此更改后,LiveData
对象会向界面通知数据中的更改,因此不再需要LiveData
观察者方法。
在第 5 课的 Codelab 中,您将从起始代码开始开发 GuessTheWord 应用。GuessTheWord 是一款双人猜字谜游戏,玩家在游戏中可以协作以获得最高得分。
第一位玩家查看应用中的单词,然后依次表演每个单词,确保不让第二位玩家看到单词。第二位玩家尝试猜出该单词。
要玩这个游戏,第一个玩家需要在设备上打开应用,然后会看到一个字词,例如“吉他”,如下面的屏幕截图所示。
第一位玩家表演该字词,但要小心不要实际说出该字词。
- 当第二位玩家正确猜出单词后,第一位玩家会按 Got It 按钮,这会将计数增加 1,并显示下一个单词。
- 如果第二位玩家猜不出单词,第一位玩家可以按跳过按钮,这样一来,计数器会减 1,并跳到下一个单词。
- 如需结束游戏,请按 End Game 按钮。(此功能未包含在本系列第一个 Codelab 的起始代码中。)
在此 Codelab 中,您将通过将数据绑定与 ViewModel
对象中的 LiveData
集成来改进 GuessTheWord 应用。这可自动实现布局中的视图与 ViewModel
对象之间的通信,并让您能够通过使用 LiveData
简化代码。
广告标题画面 | 游戏界面 | 得分界面 |
在此任务中,您将找到并运行此 Codelab 的起始代码。您可以使用在上一个 Codelab 中构建的 GuessTheWord 应用作为起始代码,也可以下载起始应用。
- (可选)如果您不使用上一个 Codelab 中的代码,请下载此 Codelab 的起始代码。解压缩代码,然后在 Android Studio 中打开项目。
- 运行应用并玩游戏。
- 请注意,点按 Got It 按钮后会显示下一个单词,并且得分会增加 1 分;而点按 Skip 按钮后会显示下一个单词,并且得分会减少 1 分。结束游戏按钮用于结束游戏。
- 循环浏览所有单词,并注意应用会自动导航到得分界面。
在之前的 Codelab 中,您使用数据绑定以类型安全的方式访问 GuessTheWord 应用中的视图。但数据绑定的真正强大之处在于,它可以按照名称所示,将数据直接绑定到应用中的视图对象。
当前应用架构
在应用中,视图在 XML 布局中定义,这些视图的数据保存在 ViewModel
对象中。每个视图与其对应的 ViewModel
之间都有一个界面控制器,充当它们之间的中继。
例如:
- 知道了按钮在
game_fragment.xml
布局文件中定义为Button
视图。 - 当用户点按知道了按钮时,
GameFragment
fragment 中的点击监听器会调用GameViewModel
中的相应点击监听器。 - 得分会在
GameViewModel
中更新。
Button
视图和 GameViewModel
不会直接通信,它们需要 GameFragment
中的点击监听器。
传递到数据绑定中的 ViewModel
如果布局中的视图可以直接与 ViewModel
对象中的数据通信,而无需依赖界面控制器作为中介,那么情况会更简单。
ViewModel
对象保存了 GuessTheWord 应用中的所有界面数据。通过将 ViewModel
对象传递到数据绑定中,您可以自动执行视图与 ViewModel
对象之间的一些通信。
在此任务中,您将 GameViewModel
和 ScoreViewModel
类与其对应的 XML 布局相关联。您还设置了监听器绑定来处理点击事件。
第 1 步:为 GameViewModel 添加数据绑定
在此步骤中,您将 GameViewModel
与相应的布局文件 game_fragment.xml
相关联。
- 在
game_fragment.xml
文件中,添加一个类型为GameViewModel
的数据绑定变量。如果您在 Android Studio 中遇到错误,请清理并重新构建项目。
<layout ...>
<data>
<variable
name="gameViewModel"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
<androidx.constraintlayout...
- 在
GameFragment
文件中,将GameViewModel
传递到数据绑定中。
为此,请将viewModel
分配给您在上一步中声明的binding.gameViewModel
变量。将以下代码放在onCreateView()
内,在viewModel
初始化之后。如果您在 Android Studio 中遇到错误,请清理并重新构建项目。
// Set the viewmodel for databinding - this allows the bound layout access
// to all the data in the ViewModel
binding.gameViewModel = viewModel
第 2 步:使用监听器绑定处理事件
监听器绑定是在触发 onClick()
、onZoomIn()
或 onZoomOut()
等事件时运行的绑定表达式。监听器绑定编写为 lambda 表达式。
数据绑定会创建一个监听器,并将其设置在视图上。当监听的事件发生时,监听器会对 lambda 表达式进行求值。监听器绑定适用于 Android Gradle 插件 2.0 或更高版本。如需了解详情,请参阅布局和绑定表达式。
在此步骤中,您需要将 GameFragment
中的点击监听器替换为 game_fragment.xml
文件中的监听器绑定。
- 在
game_fragment.xml
中,将onClick
属性添加到skip_button
。定义绑定表达式,并在GameViewModel
中调用onSkip()
方法。此绑定表达式称为监听器绑定。
<Button
android:id="@+id/skip_button"
...
android:onClick="@{() -> gameViewModel.onSkip()}"
... />
- 同样,将
correct_button
的点击事件绑定到GameViewModel
中的onCorrect
()
方法。
<Button
android:id="@+id/correct_button"
...
android:onClick="@{() -> gameViewModel.onCorrect()}"
... />
- 将
end_game_button
的点击事件绑定到GameViewModel
中的onGameFinish
()
方法。
<Button
android:id="@+id/end_game_button"
...
android:onClick="@{() -> gameViewModel.onGameFinish()}"
... />
- 在
GameFragment
中,移除用于设置点击监听器的语句,并移除点击监听器调用的函数。您不再需要这些信息。
要移除的代码:
binding.correctButton.setOnClickListener { onCorrect() }
binding.skipButton.setOnClickListener { onSkip() }
binding.endGameButton.setOnClickListener { onEndGame() }
/** Methods for buttons presses **/
private fun onSkip() {
viewModel.onSkip()
}
private fun onCorrect() {
viewModel.onCorrect()
}
private fun onEndGame() {
gameFinished()
}
第 3 步:为 ScoreViewModel 添加数据绑定
在此步骤中,您将 ScoreViewModel
与相应的布局文件 score_fragment.xml
相关联。
- 在
score_fragment.xml
文件中,添加ScoreViewModel
类型的绑定变量。此步骤与上面针对GameViewModel
执行的操作类似。
<layout ...>
<data>
<variable
name="scoreViewModel"
type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
- 在
score_fragment.xml
中,将onClick
属性添加到play_again_button
。定义监听器绑定,并在ScoreViewModel
中调用onPlayAgain()
方法。
<Button
android:id="@+id/play_again_button"
...
android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
... />
- 在
ScoreFragment
的onCreateView()
内,初始化viewModel
。然后,初始化binding.scoreViewModel
绑定变量。
viewModel = ...
binding.scoreViewModel = viewModel
- 在
ScoreFragment
中,移除为playAgainButton
设置点击监听器的代码。如果 Android Studio 显示错误,请清理并重建项目。
要移除的代码:
binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() }
- 运行应用。应用应该会像以前一样运行,但现在按钮视图会直接与
ViewModel
对象通信。视图不再通过ScoreFragment
中的按钮点击处理程序进行通信。
排查数据绑定错误消息
当应用使用数据绑定时,编译过程会生成用于数据绑定的中间类。应用可能会出现一些错误,在您尝试编译应用之前,Android Studio 不会检测到这些错误,因此您在编写代码时不会看到警告或红色代码。但在编译时,您会收到来自生成的中间类的神秘错误。
如果您收到难以理解的错误消息,请执行以下操作:
- 仔细查看 Android Studio Build 窗格中的消息。如果您看到以
databinding
结尾的位置,则表示数据绑定存在错误。 - 在布局 XML 文件中,检查使用数据绑定的
onClick
属性是否存在错误。找到 lambda 表达式调用的函数,并确保该函数存在。 - 在 XML 的
<data>
部分中,检查数据绑定变量的拼写。
例如,请注意以下属性值中函数名称 onCorrect()
的拼写错误:
android:onClick="@{() -> gameViewModel.onCorrectx()}"
另请注意 XML 文件 <data>
部分中 gameViewModel
的拼写错误:
<data>
<variable
name="gameViewModelx"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
在您编译应用之前,Android Studio 不会检测到此类错误,然后编译器会显示如下所示的错误消息:
error: cannot find symbol import com.example.android.guesstheword.databinding.GameFragmentBindingImpl" symbol: class GameFragmentBindingImpl location: package com.example.android.guesstheword.databinding
数据绑定非常适合与 ViewModel
对象搭配使用的 LiveData
。现在,您已向 ViewModel
对象添加了数据绑定,接下来可以纳入 LiveData
了。
在此任务中,您将更改 GuessTheWord 应用,使其使用 LiveData
作为数据绑定来源,以将数据变化通知给界面,而无需使用 LiveData
观察者方法。
第 1 步:向 game_fragment.xml 文件添加 word LiveData
在此步骤中,您要将当前单词文本视图直接绑定到 ViewModel
中的 LiveData
对象。
- 在
game_fragment.xml
中,将android:text
属性添加到word_text
文本视图。
使用绑定变量 gameViewModel
将其设置为 GameViewModel
中的 LiveData
对象 word
。
<TextView
android:id="@+id/word_text"
...
android:text="@{gameViewModel.word}"
... />
请注意,您无需使用 word.value
。您可以改用实际的 LiveData
对象。LiveData
对象显示 word
的当前值。如果 word
的值为 null,则 LiveData
对象会显示一个空字符串。
- 在
GameFragment
的onCreateView()
中,初始化gameViewModel
后,将当前 activity 设置为binding
变量的生命周期所有者。这定义了上述LiveData
对象的范围,允许该对象自动更新布局中的视图,game_fragment.xml
。
binding.gameViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
- 在
GameFragment
中,移除LiveData
word
的观察器。
要移除的代码:
/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
binding.wordText.text = newWord
})
- 运行应用并玩游戏。现在,系统会在界面控制器中更新当前字词,而无需使用观察器方法。
第 2 步:将得分 LiveData 添加到 score_fragment.xml 文件
在此步骤中,您将 LiveData
score
绑定到得分 fragment 中的得分文本视图。
- 在
score_fragment.xml
中,将android:text
属性添加到得分文本视图。将scoreViewModel.score
分配给text
属性。由于score
是整数,因此使用String.valueOf()
将其转换为字符串。
<TextView
android:id="@+id/score_text"
...
android:text="@{String.valueOf(scoreViewModel.score)}"
... />
- 在
ScoreFragment
中,初始化scoreViewModel
后,将当前 activity 设置为binding
变量的生命周期所有者。
binding.scoreViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
- 在
ScoreFragment
中,移除score
对象的观察者。
要移除的代码:
// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
- 运行应用并玩游戏。请注意,得分 fragment 中的得分显示正确,且得分 fragment 中没有观察者。
第 3 步:使用数据绑定添加字符串格式设置
在布局中,您可以添加字符串格式设置以及数据绑定。在此任务中,您将格式化当前字词,为其添加引号。您还可以设置得分字符串的格式,使其以 Current Score 为前缀,如下图所示。
- 在
string.xml
中,添加以下字符串,您将使用这些字符串来格式化word
和score
文本视图。%s
和%d
是当前字词和当前得分的占位符。
<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>
- 在
game_fragment.xml
中,更新word_text
文本视图的text
属性,以使用quote_format
字符串资源。传入gameViewModel.word
。这会将当前字词作为实参传递给格式设置字符串。
<TextView
android:id="@+id/word_text"
...
android:text="@{@string/quote_format(gameViewModel.word)}"
... />
- 设置
score
文本视图的格式,使其类似于word_text
。在game_fragment.xml
中,将text
属性添加到score_text
文本视图。使用字符串资源score_format
,该资源接受一个数值实参,由%d
占位符表示。将LiveData
对象score
作为实参传递给此格式化字符串。
<TextView
android:id="@+id/score_text"
...
android:text="@{@string/score_format(gameViewModel.score)}"
... />
- 在
GameFragment
类中,移除onCreateView()
方法内的score
观察器代码。
要移除的代码:
viewModel.score.observe(this, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
- 清理、重新构建并运行应用,然后玩游戏。请注意,游戏界面中当前单词和得分的格式。
恭喜!您已在应用中将 LiveData
和 ViewModel
与数据绑定集成。这样,布局中的视图就可以直接与 ViewModel
通信,而无需使用 fragment 中的点击处理程序。您还使用 LiveData
对象作为数据绑定来源,自动将数据变化通知给界面,而无需使用 LiveData
观察器方法。
Android Studio 项目:GuessTheWord
- 数据绑定库可与
ViewModel
和LiveData
等 Android 架构组件无缝协作。 - 应用中的布局可以绑定到架构组件中的数据,这些组件已经可帮助您管理界面控制器的生命周期并通知数据变化。
ViewModel 数据绑定
- 您可以使用数据绑定将
ViewModel
与布局相关联。 ViewModel
对象用于保存界面数据。通过将ViewModel
对象传递到数据绑定中,您可以自动执行视图与ViewModel
对象之间的一些通信。
如何将 ViewModel
与布局相关联:
- 在布局文件中,添加一个类型为
ViewModel
的数据绑定变量。
<data>
<variable
name="gameViewModel"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
- 在
GameFragment
文件中,将GameViewModel
传递到数据绑定中。
binding.gameViewModel = viewModel
监听器绑定
- 监听器绑定是布局中在触发点击事件(例如
onClick()
)时运行的绑定表达式。 - 监听器绑定编写为 lambda 表达式。
- 使用监听器绑定,将界面控制器中的点击监听器替换为布局文件中的监听器绑定。
- 数据绑定会创建一个监听器,并将其设置在视图上。
android:onClick="@{() -> gameViewModel.onSkip()}"
向数据绑定添加 LiveData
LiveData
对象可用作数据绑定来源,以自动将数据变化通知给界面。- 您可以将视图直接绑定到
ViewModel
中的LiveData
对象。当ViewModel
中的LiveData
发生更改时,布局中的视图可以自动更新,而无需界面控制器中的观察器方法。
android:text="@{gameViewModel.word}"
- 为了使
LiveData
数据绑定正常运行,请将当前 activity(界面控制器)设置为界面控制器中binding
变量的生命周期所有者。
binding.lifecycleOwner = this
使用数据绑定设置字符串格式
- 使用数据绑定,您可以格式化带有占位符(例如
%s
表示字符串,%d
表示整数)的字符串资源。 - 如需更新视图的
text
属性,请将LiveData
对象作为实参传递给格式化字符串。
android:text="@{@string/quote_format(gameViewModel.word)}"
Udacity 课程:
Android 开发者文档:
此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:
- 根据需要布置作业。
- 告知学生如何提交家庭作业。
- 给家庭作业评分。
讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。
如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。
回答以下问题
问题 1
以下关于监听器绑定的表述中哪一项是不正确的?
- 监听器绑定是在事件发生时运行的绑定表达式。
- 监听器绑定适用于所有版本的 Android Gradle 插件。
- 监听器绑定编写为 lambda 表达式。
- 监听器绑定类似于方法引用,但允许您运行任意数据绑定表达式。
问题 2
假设您的应用包含以下字符串资源:<string name="generic_name">Hello %s</string>
以下哪一项是使用数据绑定表达式设置字符串格式的正确语法?
android:text= "@{@string/generic_name(user.name)}"
android:text= "@{string/generic_name(user.name)}"
android:text= "@{@generic_name(user.name)}"
android:text= "@{@string/generic_name,user.name}"
问题 3
何时会评估并运行监听器绑定表达式?
- 当
LiveData
保留的数据发生更改时 - 当因配置更改而重新创建 activity 时
- 当发生
onClick()
等事件时 - 当 activity 进入后台时
开始学习下一课:
如需本课程中其他 Codelab 的链接,请参阅“Android Kotlin 基础知识”Codelab 着陆页。