Android Kotlin 基础知识 06.3:使用 LiveData 控制按钮状态

此 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 步:检查代码

  1. 首先,请继续使用上一个 Codelab 结束时的代码,或下载起始代码
  2. 在起始代码中,检查 SleepQualityFragment。此类会扩充布局、获取应用,并返回 binding.root
  3. 在设计编辑器中打开 navigation.xml。您会看到从 SleepTrackerFragmentSleepQualityFragment 的导航路径,以及从 SleepQualityFragment 返回 SleepTrackerFragment 的导航路径。



  4. 检查 navigation.xml 的代码。尤其要注意查找名为 sleepNightKey<argument>

    当用户从 SleepTrackerFragment 前往 SleepQualityFragment, 时,应用会将 sleepNightKey 传递给 SleepQualityFragment,以更新相应晚上的睡眠数据。

第 2 步:添加睡眠质量跟踪的导航

导航图已包含从 SleepTrackerFragmentSleepQualityFragment 以及从 SleepQualityFragment 返回 SleepTrackerFragment 的路径。不过,实现从一个 fragment 导航到下一个 fragment 的点击处理程序尚未编码。现在,您可以在 ViewModel 中添加该代码。

在点击处理程序中,您可以设置一个 LiveData,当您希望应用导航到其他目的地时,该 LiveData 会发生变化。fragment 会观测此 LiveData。当数据发生变化时,fragment 会导航到目的地并告知视图模型它已完成操作,这会重置状态变量。

  1. 打开 SleepTrackerViewModel。您需要添加导航功能,以便当用户点按 Stop 按钮时,应用会导航到 SleepQualityFragment 以收集质量评分。
  2. SleepTrackerViewModel 中,创建一个 LiveData,用于在您希望应用导航到 SleepQualityFragment 时进行更改。使用封装仅向 ViewModel 公开 LiveData 的可获取版本。

    您可以将此代码放置在类正文的顶层任意位置。
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()

val navigateToSleepQuality: LiveData<SleepNight>
   get() = _navigateToSleepQuality
  1. 添加一个 doneNavigating() 函数,用于重置触发导航的变量。
fun doneNavigating() {
   _navigateToSleepQuality.value = null
}
  1. Stop 按钮的点击处理程序 onStopTracking() 中,触发向 SleepQualityFragment 的导航。在函数末尾,将 _navigateToSleepQuality 变量设置为 launch{} 代码块中的最后一项。请注意,此变量设置为 night。当此变量具有值时,应用会导航到 SleepQualityFragment,并传递 night.
_navigateToSleepQuality.value = oldNight
  1. SleepTrackerFragment 需要观测 _navigateToSleepQuality,以便应用知道何时进行导航。在 SleepTrackerFragmentonCreateView() 中,为 navigateToSleepQuality() 添加一个观察器。请注意,此导入不明确,您需要导入 androidx.lifecycle.Observer
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})

  1. 在观察器块内,导航并传递当前夜晚的 ID,然后调用 doneNavigating()。如果导入不明确,请导入 androidx.navigation.fragment.findNavController
night ->
night?.let {
   this.findNavController().navigate(
           SleepTrackerFragmentDirections
                   .actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
   sleepTrackerViewModel.doneNavigating()
}
  1. 构建并运行您的应用。依次点按 StartStop,系统会将您带到 SleepQualityFragment 界面。如需返回,请使用系统返回按钮。

在此任务中,您将记录睡眠质量并返回到睡眠跟踪器 fragment。显示内容应自动更新,以向用户显示更新后的值。您需要创建 ViewModelViewModelFactory,并更新 SleepQualityFragment

第 1 步:创建 ViewModel 和 ViewModelFactory

  1. sleepquality 软件包中,创建或打开 SleepQualityViewModel.kt
  2. 创建一个以 sleepNightKey 和数据库作为实参的 SleepQualityViewModel 类。与 SleepTrackerViewModel 的情况一样,您需要从工厂传入 database。您还需要从导航中传入 sleepNightKey
class SleepQualityViewModel(
       private val sleepNightKey: Long = 0L,
       val database: SleepDatabaseDao) : ViewModel() {
}
  1. SleepQualityViewModel 类中,定义 JobuiScope,并替换 onCleared()
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. 如需使用与上述相同的模式返回到 SleepTrackerFragment,请声明 _navigateToSleepTracker。实现 navigateToSleepTrackerdoneNavigating()
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()

val navigateToSleepTracker: LiveData<Boolean?>
   get() = _navigateToSleepTracker

fun doneNavigating() {
   _navigateToSleepTracker.value = null
}
  1. 为所有睡眠质量图片创建一个点击处理程序 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
        }
    }
  1. 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

  1. 打开 SleepQualityFragment.kt
  2. onCreateView() 中,获取 application 后,您需要获取导航随附的 arguments。这些实参位于 SleepQualityFragmentArgs 中。您需要从软件包中提取这些文件。
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
  1. 接下来,获取 dataSource
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. 创建一个工厂,传入 dataSourcesleepNightKey
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
  1. 获取 ViewModel 引用。
val sleepQualityViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepQualityViewModel::class.java)
  1. ViewModel 添加到绑定对象。(如果您看到与绑定对象相关的错误,请暂时忽略。)
binding.sleepQualityViewModel = sleepQualityViewModel
  1. 添加观察者。出现提示时,导入 androidx.lifecycle.Observer
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
   if (it == true) { // Observed state is true.
       this.findNavController().navigate(
               SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
       sleepQualityViewModel.doneNavigating()
   }
})

第 3 步:更新布局文件并运行应用

  1. 打开 fragment_sleep_quality.xml 布局文件。在 <data> 代码块中,为 SleepQualityViewModel 添加一个变量。
 <data>
       <variable
           name="sleepQualityViewModel"
           type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
   </data>
  1. 为六张睡眠质量图片中的每一张添加如下所示的点击处理程序。将质量评级与图片相匹配。
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
  1. 清理并重建项目。这应该可以解决绑定对象的所有错误。否则,请清除缓存(依次选择 File > Invalidate Caches / Restart),然后重新构建应用。

恭喜!您刚刚使用协程构建了一个完整的 Room 数据库应用。

现在,您的应用可以正常运行了。用户可以根据需要多次点按开始停止。当用户点按停止时,可以输入睡眠质量。当用户点按 Clear 时,系统会在后台静默清除所有数据。不过,所有按钮始终处于启用状态且可点击,这不会导致应用崩溃,但允许用户创建不完整的睡眠夜。

在此最后一项任务中,您将学习如何使用转换映射来管理按钮的可见性,以便用户只能做出正确的选择。您可以使用类似的方法在清除所有数据后显示一条友好消息。

第 1 步:更新按钮状态

我们的想法是设置按钮状态,以便在开始时仅启用 Start 按钮,这意味着该按钮可点击。

用户点按 Start 后,Stop 按钮会变为启用状态,而 Start 按钮则不会。只有当数据库中有数据时,CLEAR 按钮才会处于启用状态。

  1. 打开 fragment_sleep_tracker.xml 布局文件。
  2. 为每个按钮添加 android:enabled 属性。android:enabled 属性是一个布尔值,用于指示按钮是否处于启用状态。(已启用的按钮可以点按,已停用的按钮则无法点按。)为该属性赋予稍后将定义的状态变量的值。

start_button

android:enabled="@{sleepTrackerViewModel.startButtonVisible}"

stop_button:

android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"

clear_button

android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
  1. 打开 SleepTrackerViewModel 并创建三个对应的变量。为每个变量分配一个用于测试该变量的转换。
  • tonightnull 时,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()
}
  1. 运行应用,然后尝试使用这些按钮。

第 2 步:使用 snackbar 通知用户

在用户清除数据库后,使用 Snackbar widget 向用户显示确认消息。信息条可通过屏幕底部的消息提供与操作相关的简短反馈。在超时后、用户在屏幕上其他位置互动后或用户将 snackbar 滑出屏幕后,snackbar 会消失。

显示 snackbar 是一项界面任务,应在 fragment 中进行。决定是否显示 snackbar 的操作在 ViewModel 中进行。如需在数据清除时设置并触发 snackbar,您可以使用与触发导航相同的技巧。

  1. SleepTrackerViewModel 中,创建封装的事件。
private var _showSnackbarEvent = MutableLiveData<Boolean>()

val showSnackBarEvent: LiveData<Boolean>
   get() = _showSnackbarEvent
  1. 然后实现 doneShowingSnackbar()
fun doneShowingSnackbar() {
   _showSnackbarEvent.value = false
}
  1. SleepTrackerFragmentonCreateView() 中,添加一个观察器:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
  1. 在观察器代码块内,显示 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()
   }
  1. SleepTrackerViewModel 中,在 onClear() 方法中触发事件。为此,请在 launch 代码块内将事件值设置为 true
_showSnackbarEvent.value = true
  1. 构建并运行您的应用!

Android Studio 项目:TrackMySleepQualityFinal

在此应用中实现睡眠质量跟踪功能,就像用新的调演奏一首熟悉的乐曲。虽然细节有所变化,但您在本课程中之前完成的 Codelab 的基本模式保持不变。了解这些模式可让您更快地编写代码,因为您可以重复使用现有应用中的代码。以下是本课程目前为止使用的一些模式:

  • 创建 ViewModelViewModelFactory 并设置数据源。
  • 触发导航。为了分离关注点,请将点击处理程序放在视图模型中,并将导航放在 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 中,定义 LiveDatagotoBlueFragment
  • RedFragment 中,观察 gotoBlueFragment 值。实现 observe{} 代码,以便在适当的时候导航到 BlueFragment,然后重置 gotoBlueFragment 的值以指示导航已完成。
  • 确保您的代码将 gotoBlueFragment 变量设置为触发导航的值,以便应用在需要从 RedFragment 切换到 BlueFragment 时进行导航。
  • 确保您的代码为用户点击以导航到 BlueFragmentView 定义了 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 buttonandroid: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 buttonandroid:enabled 属性设置为 "Observable"

开始学习下一课:7.1 RecyclerView 基础知识

如需本课程中其他 Codelab 的链接,请参阅“Android Kotlin 基础知识”Codelab 着陆页