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 跟踪按钮状态。
  • 如何在响应事件时显示信息提示控件。

您将执行的操作

  • 扩展 TrackMySleepQuality 应用以收集质量评分,将评分添加到数据库中,并显示结果。
  • 使用 LiveData 触发信息提示控件。
  • 使用 LiveData 启用和停用按钮。

在此 Codelab 中,您将为 TrackMySleepQuality 应用构建睡眠质量记录和最终界面。

该应用包含两个由 Fragment 表示的屏幕,如下图所示。

左侧所示的第一个屏幕包含用于开始和停止跟踪的按钮。这个屏幕会显示用户的所有睡眠数据。点击清除按钮会永久删除应用为用户收集的所有数据。

右侧所示的第二个屏幕用于选择睡眠质量评分。在该应用中,评分用数字表示。出于开发目的,该应用会显示人脸图标及其对应的数字。

用户的流程如下所示:

  • 用户打开应用,系统会显示睡眠跟踪屏幕。
  • 用户点按 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>

    当用户从 SleepTrackerFragmentSleepQualityFragment, 时,应用便会将 sleepNightKey 传递到 SleepQualityFragment,以便应用进行更新之夜。

第 2 步:添加用于监测睡眠质量的导航功能

导航图已包含从 SleepTrackerFragmentSleepQualityFragment 再返回的路径。不过,用于实现从一个 fragment 导航到下一个 fragment 的点击处理程序还没有编码。现在,将该代码添加到 ViewModel 中。

在点击处理程序中设置 LiveData,以便在应用导航到其他目的地时更改。fragment 会观察此 LiveData。当数据发生更改时,fragment 将导航到目的地并告知视图模型它已完成,这会重置状态变量。

  1. 打开 SleepTrackerViewModel。您需要添加导航功能,以便在用户点按 Stop 按钮时应用导航到 SleepQualityFragment 收集质量评分。
  2. SleepTrackerViewModel 中,创建一个 LiveData,规定您希望应用导航到 SleepQualityFragment 时。使用封装仅对 ViewModel 提供 LiveData 的 getable 版本。

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

val navigateToSleepQuality: LiveData<SleepNight>
   get() = _navigateToSleepQuality
  1. 添加一个 doneNavigating() 函数,用于重置触发导航的变量。
fun doneNavigating() {
   _navigateToSleepQuality.value = null
}
  1. 停止按钮 onStopTracking() 的点击处理程序中,触发导航到 SleepQualityFragment 的操作。将函数末尾的 _navigateToSleepQuality 变量设置为 launch{} 代码块内的最后一项。请注意,此变量设置为 night。当此变量具有值时,应用便会导航到 SleepQualityFragment,并沿途传递。
_navigateToSleepQuality.value = oldNight
  1. SleepTrackerFragment 需要观察 _navigateToSleepQuality,以便应用知道何时导航。在 SleepTrackerFragment 中的 onCreateView() 内,为 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. 构建并运行您的应用。点按开始,然后点按停止,系统会将您转到 SleepQualityFragment 屏幕。如需返回,请使用系统的“返回”按钮。

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

第 1 步:创建 ViewModel 和 ViewModelFactory

  1. sleepquality 软件包中,创建或打开 SleepQualityViewModel.kt
  2. 创建一个 SleepQualityViewModel 类,该类将 sleepNightKey 和数据库作为参数。与处理 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 中。您需要从 bundle 中提取它们。
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 数据库应用。

现在,您的应用可以正常运行了。用户可以点按开始停止任意次数。当用户点按停止时,他们可以输入睡眠质量。当用户点按清除时,系统会在后台以静默方式清除所有数据。不过,所有按钮都始终处于启用状态,且可点击,这不会破坏应用,但确实会造成用户睡觉时不完整。

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

第 1 步:更新按钮状态

其思路是设置按钮状态,以便最初只启用开始按钮,也就是说,该按钮是可点击的。

用户点按开始后,停止按钮会变为启用状态,而开始按钮不会变为启用状态。只有在数据库中存在数据时,清除按钮才会启用。

  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 时,应启用开始按钮。
  • 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 微件向用户显示确认消息。信息提示控件通过屏幕底部的消息提供有关操作的简短反馈。快捷信息栏在超时后、用户与屏幕上的其他位置互动或用户将信息提示控件从屏幕中滑出后消失。

显示信息提示控件是一项界面任务,应该发生在 fragment 中。在 ViewModel 中会显示快捷信息栏。若要在数据清除后设置并触发信息提示控件,您可以使用与触发导航相同的方法。

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

val showSnackBarEvent: LiveData<Boolean>
   get() = _showSnackbarEvent
  1. 然后实现 doneShowingSnackbar()
fun doneShowingSnackbar() {
   _showSnackbarEvent.value = false
}
  1. SleepTrackerFragment 中的 onCreateView() 中,添加观察器:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
  1. 在观察器块内,显示信息提示控件,并立即重置事件。
   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。“enabled”的含义因子类而异。例如,未启用的 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 时触发导航的值。
  • 请确保您的代码为 View 定义了一个 onClick 处理程序,用户点击以导航到 BlueFragment,其中 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 着陆页