此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。
简介
此 Codelab 总结了如何将 ViewModel
与 fragment 搭配使用以实现导航。请注意,我们的目标是将导航的 when 逻辑放入 ViewModel
中,但在 fragment 和导航文件中定义路径。为实现此目标,您需要使用视图模型、fragment、LiveData
和观察器。
此 Codelab 最后展示了一种巧妙的方法,只需极少的代码即可跟踪按钮状态,从而确保只有在用户可以点击相应按钮时,该按钮才处于启用状态且可供点击。
您应当已掌握的内容
您应熟悉以下内容:
- 使用 activity、fragment 和视图构建基本界面 (UI)。
- 在 fragment 之间导航,并使用
safeArgs
在 fragment 之间传递数据。 - 视图模型、视图模型工厂、转换以及
LiveData
及其观察器。 - 如何创建
Room
数据库、创建数据访问对象 (DAO) 以及定义实体。 - 如何将协程用于数据库互动和其他长时间运行的任务。
学习内容
- 如何更新数据库中的现有睡眠质量记录。
- 如何使用
LiveData
跟踪按钮状态。 - 如何显示 snackbar 以响应事件。
您将执行的操作
- 扩展 TrackMySleepQuality 应用以收集质量评分,将评分添加到数据库中,并显示结果。
- 使用
LiveData
触发信息提示控件的显示。 - 使用
LiveData
启用和停用按钮。
在此 Codelab 中,您将构建 TrackMySleepQuality 应用的睡眠质量记录和最终界面。
该应用有两个屏幕,以 fragment 表示,如下图所示。
左侧所示的第一个屏幕包含用于开始和停止跟踪的按钮。这个屏幕会显示用户的所有睡眠数据。CLEAR 按钮用于永久删除应用针对用户收集的所有数据。
右侧所示的第二个屏幕用于选择睡眠质量评分。在该应用中,评分用数字表示。出于开发目的,该应用同时显示人脸图标及其对应的数字。
用户的流程如下所示:
- 用户打开该应用,并看到睡眠跟踪界面。
- 用户点按 START 按钮。系统会记录开始时间并显示该时间。START 按钮会停用,而 STOP 按钮会启用。
- 用户点按 STOP 按钮。系统会记录结束时间,并打开睡眠质量界面。
- 用户选择一个睡眠质量图标。这个屏幕会关闭,跟踪屏幕会显示睡眠结束时间和睡眠质量。STOP 按钮会停用,而 START 按钮会启用。该应用已为下一晚运行做好准备。
- 只要数据库中有数据,CLEAR 按钮就会处于启用状态。如果用户点按 CLEAR 按钮,系统会清空其所有数据,并且不予追偿,也就是说,系统不会显示“您确定吗?”这类消息。
该应用在完整架构的基础上采用简化的架构,如下所示。该应用仅使用以下组件:
- 界面控制器
- 视图模型和
LiveData
- Room 数据库
本 Codelab 假定您知道如何使用 fragment 和导航文件来实现导航。为了节省您的工作量,我们提供了大量代码。
第 1 步:检查代码
- 首先,请继续使用上一个 Codelab 结束时的代码,或下载起始代码。
- 在起始代码中,检查
SleepQualityFragment
。此类会扩充布局、获取应用,并返回binding.root
。 - 在设计编辑器中打开 navigation.xml。您会看到从
SleepTrackerFragment
到SleepQualityFragment
的导航路径,以及从SleepQualityFragment
返回SleepTrackerFragment
的导航路径。 - 检查 navigation.xml 的代码。尤其要注意查找名为
sleepNightKey
的<argument>
。
当用户从SleepTrackerFragment
前往SleepQualityFragment,
时,应用会将sleepNightKey
传递给SleepQualityFragment
,以更新相应晚上的睡眠数据。
第 2 步:添加睡眠质量跟踪的导航
导航图已包含从 SleepTrackerFragment
到 SleepQualityFragment
以及从 SleepQualityFragment
返回 SleepTrackerFragment
的路径。不过,实现从一个 fragment 导航到下一个 fragment 的点击处理程序尚未编码。现在,您可以在 ViewModel
中添加该代码。
在点击处理程序中,您可以设置一个 LiveData
,当您希望应用导航到其他目的地时,该 LiveData
会发生变化。fragment 会观测此 LiveData
。当数据发生变化时,fragment 会导航到目的地并告知视图模型它已完成操作,这会重置状态变量。
- 打开
SleepTrackerViewModel
。您需要添加导航功能,以便当用户点按 Stop 按钮时,应用会导航到SleepQualityFragment
以收集质量评分。 - 在
SleepTrackerViewModel
中,创建一个LiveData
,用于在您希望应用导航到SleepQualityFragment
时进行更改。使用封装仅向ViewModel
公开LiveData
的可获取版本。
您可以将此代码放置在类正文的顶层任意位置。
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()
val navigateToSleepQuality: LiveData<SleepNight>
get() = _navigateToSleepQuality
- 添加一个
doneNavigating()
函数,用于重置触发导航的变量。
fun doneNavigating() {
_navigateToSleepQuality.value = null
}
- 在 Stop 按钮的点击处理程序
onStopTracking()
中,触发向SleepQualityFragment
的导航。在函数末尾,将 _navigateToSleepQuality
变量设置为launch{}
代码块中的最后一项。请注意,此变量设置为night
。当此变量具有值时,应用会导航到SleepQualityFragment
,并传递 night.
。
_navigateToSleepQuality.value = oldNight
SleepTrackerFragment
需要观测 _navigateToSleepQuality
,以便应用知道何时进行导航。在SleepTrackerFragment
的onCreateView()
中,为navigateToSleepQuality()
添加一个观察器。请注意,此导入不明确,您需要导入androidx.lifecycle.Observer
。
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})
- 在观察器块内,导航并传递当前夜晚的 ID,然后调用
doneNavigating()
。如果导入不明确,请导入androidx.navigation.fragment.findNavController
。
night ->
night?.let {
this.findNavController().navigate(
SleepTrackerFragmentDirections
.actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
sleepTrackerViewModel.doneNavigating()
}
- 构建并运行您的应用。依次点按 Start 和 Stop,系统会将您带到
SleepQualityFragment
界面。如需返回,请使用系统返回按钮。
在此任务中,您将记录睡眠质量并返回到睡眠跟踪器 fragment。显示内容应自动更新,以向用户显示更新后的值。您需要创建 ViewModel
和 ViewModelFactory
,并更新 SleepQualityFragment
。
第 1 步:创建 ViewModel 和 ViewModelFactory
- 在
sleepquality
软件包中,创建或打开 SleepQualityViewModel.kt。 - 创建一个以
sleepNightKey
和数据库作为实参的SleepQualityViewModel
类。与SleepTrackerViewModel
的情况一样,您需要从工厂传入database
。您还需要从导航中传入sleepNightKey
。
class SleepQualityViewModel(
private val sleepNightKey: Long = 0L,
val database: SleepDatabaseDao) : ViewModel() {
}
- 在
SleepQualityViewModel
类中,定义Job
和uiScope
,并替换onCleared()
。
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
- 如需使用与上述相同的模式返回到
SleepTrackerFragment
,请声明_navigateToSleepTracker
。实现navigateToSleepTracker
和doneNavigating()
。
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()
val navigateToSleepTracker: LiveData<Boolean?>
get() = _navigateToSleepTracker
fun doneNavigating() {
_navigateToSleepTracker.value = null
}
- 为所有睡眠质量图片创建一个点击处理程序
onSetSleepQuality()
以供使用。
使用与上一个 Codelab 中相同的协程模式:
- 在
uiScope
中启动协程,并切换到 I/O 调度程序。 - 使用
sleepNightKey
获取tonight
。 - 设置睡眠质量。
- 更新数据库。
- 触发导航。
请注意,下面的代码示例在点击处理程序中完成了所有工作,而不是在不同上下文中提取数据库操作。
fun onSetSleepQuality(quality: Int) {
uiScope.launch {
// IO is a thread pool for running operations that access the disk, such as
// our Room database.
withContext(Dispatchers.IO) {
val tonight = database.get(sleepNightKey) ?: return@withContext
tonight.sleepQuality = quality
database.update(tonight)
}
// Setting this state variable to true will alert the observer and trigger navigation.
_navigateToSleepTracker.value = true
}
}
- 在
sleepquality
软件包中,创建或打开SleepQualityViewModelFactory.kt
,然后添加SleepQualityViewModelFactory
类,如下所示。此类使用您之前见过的相同样板代码的一个版本。请先检查代码,然后再继续。
class SleepQualityViewModelFactory(
private val sleepNightKey: Long,
private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) {
return SleepQualityViewModel(sleepNightKey, dataSource) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
第 2 步:更新 SleepQualityFragment
- 打开
SleepQualityFragment.kt
。 - 在
onCreateView()
中,获取application
后,您需要获取导航随附的arguments
。这些实参位于SleepQualityFragmentArgs
中。您需要从软件包中提取这些文件。
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
- 接下来,获取
dataSource
。
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
- 创建一个工厂,传入
dataSource
和sleepNightKey
。
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
- 获取
ViewModel
引用。
val sleepQualityViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(SleepQualityViewModel::class.java)
- 将
ViewModel
添加到绑定对象。(如果您看到与绑定对象相关的错误,请暂时忽略。)
binding.sleepQualityViewModel = sleepQualityViewModel
- 添加观察者。出现提示时,导入
androidx.lifecycle.Observer
。
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
if (it == true) { // Observed state is true.
this.findNavController().navigate(
SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
sleepQualityViewModel.doneNavigating()
}
})
第 3 步:更新布局文件并运行应用
- 打开
fragment_sleep_quality.xml
布局文件。在<data>
代码块中,为SleepQualityViewModel
添加一个变量。
<data>
<variable
name="sleepQualityViewModel"
type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
</data>
- 为六张睡眠质量图片中的每一张添加如下所示的点击处理程序。将质量评级与图片相匹配。
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
- 清理并重建项目。这应该可以解决绑定对象的所有错误。否则,请清除缓存(依次选择 File > Invalidate Caches / Restart),然后重新构建应用。
恭喜!您刚刚使用协程构建了一个完整的 Room
数据库应用。
现在,您的应用可以正常运行了。用户可以根据需要多次点按开始和停止。当用户点按停止时,可以输入睡眠质量。当用户点按 Clear 时,系统会在后台静默清除所有数据。不过,所有按钮始终处于启用状态且可点击,这不会导致应用崩溃,但允许用户创建不完整的睡眠夜。
在此最后一项任务中,您将学习如何使用转换映射来管理按钮的可见性,以便用户只能做出正确的选择。您可以使用类似的方法在清除所有数据后显示一条友好消息。
第 1 步:更新按钮状态
我们的想法是设置按钮状态,以便在开始时仅启用 Start 按钮,这意味着该按钮可点击。
用户点按 Start 后,Stop 按钮会变为启用状态,而 Start 按钮则不会。只有当数据库中有数据时,CLEAR 按钮才会处于启用状态。
- 打开
fragment_sleep_tracker.xml
布局文件。 - 为每个按钮添加
android:enabled
属性。android:enabled
属性是一个布尔值,用于指示按钮是否处于启用状态。(已启用的按钮可以点按,已停用的按钮则无法点按。)为该属性赋予稍后将定义的状态变量的值。
start_button
:
android:enabled="@{sleepTrackerViewModel.startButtonVisible}"
stop_button
:
android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"
clear_button
:
android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
- 打开
SleepTrackerViewModel
并创建三个对应的变量。为每个变量分配一个用于测试该变量的转换。
- 当
tonight
为null
时,Start 按钮应处于启用状态。 - 当
tonight
不为null
时,应启用 Stop 按钮。 - 只有当
nights
(即数据库)包含睡眠夜数时,Clear 按钮才应处于启用状态。
val startButtonVisible = Transformations.map(tonight) {
it == null
}
val stopButtonVisible = Transformations.map(tonight) {
it != null
}
val clearButtonVisible = Transformations.map(nights) {
it?.isNotEmpty()
}
- 运行应用,然后尝试使用这些按钮。
第 2 步:使用 snackbar 通知用户
在用户清除数据库后,使用 Snackbar
widget 向用户显示确认消息。信息条可通过屏幕底部的消息提供与操作相关的简短反馈。在超时后、用户在屏幕上其他位置互动后或用户将 snackbar 滑出屏幕后,snackbar 会消失。
显示 snackbar 是一项界面任务,应在 fragment 中进行。决定是否显示 snackbar 的操作在 ViewModel
中进行。如需在数据清除时设置并触发 snackbar,您可以使用与触发导航相同的技巧。
- 在
SleepTrackerViewModel
中,创建封装的事件。
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
- 然后实现
doneShowingSnackbar()
。
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
- 在
SleepTrackerFragment
的onCreateView()
中,添加一个观察器:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
- 在观察器代码块内,显示 snackbar 并立即重置事件。
if (it == true) { // Observed state is true.
Snackbar.make(
activity!!.findViewById(android.R.id.content),
getString(R.string.cleared_message),
Snackbar.LENGTH_SHORT // How long to display the message.
).show()
sleepTrackerViewModel.doneShowingSnackbar()
}
- 在
SleepTrackerViewModel
中,在onClear()
方法中触发事件。为此,请在launch
代码块内将事件值设置为true
:
_showSnackbarEvent.value = true
- 构建并运行您的应用!
Android Studio 项目:TrackMySleepQualityFinal
在此应用中实现睡眠质量跟踪功能,就像用新的调演奏一首熟悉的乐曲。虽然细节有所变化,但您在本课程中之前完成的 Codelab 的基本模式保持不变。了解这些模式可让您更快地编写代码,因为您可以重复使用现有应用中的代码。以下是本课程目前为止使用的一些模式:
- 创建
ViewModel
和ViewModelFactory
并设置数据源。 - 触发导航。为了分离关注点,请将点击处理程序放在视图模型中,并将导航放在 fragment 中。
- 使用
LiveData
进行封装,以跟踪和响应状态变化。 - 将转换与
LiveData
搭配使用。 - 创建单例数据库。
- 为数据库操作设置协程。
触发导航
您可以在导航文件中定义 fragment 之间可能的导航路径。您可以通过多种不同的方式触发从一个 fragment 到下一个 fragment 的导航。其中包括:
- 定义
onClick
处理程序以触发导航到目标 fragment。 - 或者,如需启用从一个 fragment 到下一个 fragment 的导航,请执行以下操作:
- 定义一个
LiveData
值,用于记录是否应进行导航。 - 将观察器附加到该
LiveData
值。 - 然后,您的代码会在需要触发或完成导航时更改该值。
设置 android:enabled 属性
android:enabled
属性在TextView
中定义,并由所有子类(包括Button
)继承。android:enabled
属性用于确定是否启用View
。“已启用”的含义因子类而异。例如,如果EditText
未启用,用户将无法修改其中包含的文本;如果Button
未启用,用户将无法点按相应按钮。enabled
属性与visibility
属性不同。- 您可以使用转换映射,根据其他对象或变量的状态设置按钮的
enabled
属性的值。
此 Codelab 中涵盖的其他要点:
- 如需向用户触发通知,您可以使用与触发导航相同的技巧。
- 您可以使用
Snackbar
来通知用户。
Udacity 课程:
Android 开发者文档:
此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:
- 根据需要布置作业。
- 告知学生如何提交家庭作业。
- 给家庭作业评分。
讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。
如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。
回答以下问题
问题 1
如需让应用能够从一个 fragment 触发导航到下一个 fragment,一种方法是使用 LiveData
值来指明是否触发导航。
使用 LiveData
值(称为 gotoBlueFragment
)触发从红色 fragment 到蓝色 fragment 的导航需要执行哪些步骤?选择所有适用选项:
- 在
ViewModel
中,定义LiveData
值gotoBlueFragment
。 - 在
RedFragment
中,观察gotoBlueFragment
值。实现observe{}
代码,以便在适当的时候导航到BlueFragment
,然后重置gotoBlueFragment
的值以指示导航已完成。 - 确保您的代码将
gotoBlueFragment
变量设置为触发导航的值,以便应用在需要从RedFragment
切换到BlueFragment
时进行导航。 - 确保您的代码为用户点击以导航到
BlueFragment
的View
定义了onClick
处理程序,其中onClick
处理程序会观察goToBlueFragment
值。
问题2
您可以使用 LiveData
更改 Button
是否处于启用状态(可点击)。您如何确保应用更改 UpdateNumber
按钮,以便:
- 如果
myNumber
的值大于 5,则启用该按钮。 - 如果
myNumber
小于或等于 5,则该按钮处于未启用状态。
假设包含 UpdateNumber
按钮的布局包含 NumbersViewModel
的 <data>
变量,如下所示:
<data> <variable name="NumbersViewModel" type="com.example.android.numbersapp.NumbersViewModel" /> </data>
假设布局文件中的按钮 ID 如下所示:
android:id="@+id/update_number_button"
您还需要做些什么?请选择所有适用的选项。
- 在
NumbersViewModel
类中,定义一个表示数字的LiveData
变量myNumber
。此外,还定义了一个变量,其值通过对myNumber
变量调用Transform.map()
来设置,该调用会返回一个布尔值,指示相应数字是否大于 5。
具体而言,在ViewModel
中,添加以下代码:
val myNumber: LiveData<Int>
val enableUpdateNumberButton = Transformations.map(myNumber) {
myNumber > 5
}
- 在 XML 布局中,将
update_number_button button
的android:enabled
属性设置为NumberViewModel.enableUpdateNumbersButton
。
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
- 在使用了
NumbersViewModel
类的Fragment
中,向按钮的enabled
属性添加一个观察器。
具体而言,在Fragment
中,添加以下代码:
// Observer for the enabled attribute
viewModel.enabled.observe(this, Observer<Boolean> { isEnabled ->
myNumber > 5
})
- 在布局文件中,将
update_number_button button
的android:enabled
属性设置为"Observable"
。
开始学习下一课:
如需本课程中其他 Codelab 的链接,请参阅“Android Kotlin 基础知识”Codelab 着陆页。