Android Kotlin 基础知识:6.2 协程和 Room

此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。

简介

打造完美用户体验的首要任务之一是确保应用的界面总是能够迅速响应并顺畅运行。为了带来这种体验,我们可以通过将数据库操作等长时间运行的任务移至后台来提高界面性能。

在此 Codelab 中,您将实现 TrackMySleepQuality 应用中面向用户的部分,使用 Kotlin 协程在主线程之外执行数据库操作。

您应当已掌握的内容

您应熟悉以下内容:

  • 使用 activity、fragment、视图和点击处理程序构建基本界面。
  • 在 fragment 之间导航,并使用 safeArgs 在 fragment 之间传递简单的数据。
  • 视图模型、视图模型工厂、转换和 LiveData
  • 如何创建 Room 数据库、创建 DAO 以及定义实体。
  • 基本的线程和多进程概念。

学习内容

  • 线程在 Android 中的工作原理。
  • 如何使用 Kotlin 协程将数据库操作移出主线程。
  • 如何在 TextView 中显示设置了格式的数据。

实践内容

  • 扩展 TrackMySleepQuality 应用以收集、存储和显示数据库中的数据。
  • 使用协程在后台运行长时间运行的数据库操作。
  • 使用 LiveData 触发导航和信息提示控件的显示。
  • 使用 LiveData 启用和停用按钮。

在此 Codelab 中,您将构建 TrackMySleepQuality 应用的视图模型、协程和数据显示部分。

该应用有两个屏幕,以 fragment 表示,如下图所示。

WTsRlkiNXXQ7fe3myuNV8SigzmETej7zu35m_x7VEPJNGUwKLNrSFg1k7RPur1Y6tvMcInSIKbeHysH-AvR2MYoJCkOsHWBpgyQ6ut4Bvmxa5tpX9NVMIv7lc-7gDLTw4dUSV7wFkQ

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

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

用户的流程如下所示:

  • 用户打开该应用,并看到睡眠跟踪屏幕。
  • 用户点按 START 按钮。系统会记录开始时间并显示该时间。START 按钮会停用,而 STOP 按钮会启用。
  • 用户点按 STOP 按钮。系统会记录结束时间,并打开睡眠质量屏幕。
  • 用户选择一个睡眠质量图标。这个屏幕会关闭,跟踪屏幕会显示睡眠结束时间和睡眠质量。STOP 按钮会停用,而 START 按钮会启用。该应用已为下一晚运行做好准备。
  • 只要数据库中有数据,CLEAR 按钮就会处于启用状态。如果用户点按 CLEAR 按钮,系统会清空其所有数据,并且不予追偿,也就是说,系统不会显示“您确定吗?”这类消息。

该应用在完整架构的基础上采用简化的架构,如下所示。该应用仅使用以下组件:

  • 界面控制器
  • 视图模型和 LiveData
  • Room 数据库

Q7-Cq9-Y4h13EbiYcZR---sZ74dgkqejB699RY7cCIxGresnPfPKHqjX8HsTVB24r-c3gqRAgWUrERqjraXtQPpdCZ-ZeJhtUHw9s2-j39pW9Cerk0Qethe_Pc3oOy0hVl-q9i47Xg

在此任务中,您将使用 TextView 显示设置了格式的睡眠跟踪数据。(这不是最终界面。您将在另一个 Codelab 中改进界面。)

您可以继续使用在上一个 Codelab 中构建的 TrackMySleepQuality 应用,也可以下载此 Codelab 的起始应用

第 1 步:下载并运行起始应用

  1. 从 GitHub 下载 TrackMySleepQuality-Coroutines-Starter 应用。
  2. 构建并运行应用。该应用会显示 SleepTrackerFragment fragment 的界面,但不会显示数据。按钮不会响应点按操作。

第 2 步:检查代码

此 Codelab 的起始代码与“创建 Room 数据库”Codelab 的解决方案代码相同。

  1. 打开 res/layout/activity_main.xml。此布局包含 nav_host_fragment fragment。此外,请注意 <merge> 标记。当包含布局时,merge 标记可用于消除冗余布局,使用该标记是一种不错的做法。例如,ConstraintLayout > LinearLayout > TextView 就是冗余布局,其中系统或许能够消除 LinearLayout。这种优化可以简化视图层次结构并提高应用性能。
  2. navigation 文件夹中,打开 navigation.xml。您可以看到两个 fragment 以及连接它们的导航操作。
  3. layout 文件夹中,打开 fragment_sleep_tracker.xml,然后点击 Code 视图以查看其 XML 布局。请注意以下几点:
  • 布局数据封装在 <layout> 元素中以启用数据绑定。
  • ConstraintLayout 及其他视图被安排在 <layout> 元素内。
  • 该文件具有占位符 <data> 标记。

起始应用还提供了界面的尺寸、颜色和样式设置。该应用包含 Room 数据库、DAO 和 SleepNight 实体。如果您未完成前面的“创建 Room 数据库”Codelab,请务必自行了解代码的这些方面。

现在您已经有了数据库和界面,接下来需要收集数据、将数据添加到数据库,并显示数据。所有这些工作都在视图模型中完成。您的睡眠跟踪器视图模型将处理按钮点击、通过 DAO 与数据库进行交互,并通过 LiveData 向界面提供数据。所有数据库操作都必须在主界面线程之外运行,您将使用协程做到这一点。

第 1 步:添加 SleepTrackerViewModel

  1. sleeptracker 软件包中,打开 SleepTrackerViewModel.kt
  2. 检查 SleepTrackerViewModel 类,我们在起始应用中为您提供了该类,下面也显示了该类。请注意,该类扩展了 AndroidViewModel。该类与 ViewModel 相同,但它将应用上下文作为构造函数参数,并使其可用作属性。您稍后会需要它。
class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

第 2 步:添加 SleepTrackerViewModelFactory

  1. sleeptracker 软件包中,打开 SleepTrackerViewModelFactory.kt
  2. 检查针对工厂为您提供的代码,如下所示:
class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

请注意以下几点:

  • 提供的 SleepTrackerViewModelFactory 采用的参数与 ViewModel 相同,并扩展了 ViewModelProvider.Factory
  • 在工厂内,代码替换了 create(),该方法会将任何类类型作为参数并返回 ViewModel
  • create() 的主体中,代码会检查是否有可用的 SleepTrackerViewModel 类,如果有,会返回它的实例。否则,代码会抛出异常。

第 3 步:更新 SleepTrackerFragment

  1. SleepTrackerFragment.kt 中,获取对应用上下文的引用。将该引用放在 onCreateView() 中的 binding 下面。您需要引用此 fragment 连接到的应用,以传入视图模型工厂提供程序。

如果nullrequireNotNull Kotlin 函数会抛出 IllegalArgumentException

val application = requireNotNull(this.activity).application
  1. 您需要通过引用 DAO 来引用数据源。在 onCreateView() 中的 return 前面,定义 dataSource。如需获取对数据库的 DAO 的引用,请使用 SleepDatabase.getInstance(application).sleepDatabaseDao
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. onCreateView() 中的 return 前面,创建 viewModelFactory 的实例。您需要向其传递 dataSourceapplication
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  1. 现在您已经有了工厂,接下来获取对 SleepTrackerViewModel 的引用。SleepTrackerViewModel::class.java 参数引用此对象的运行时 Java 类。
val sleepTrackerViewModel =
       ViewModelProvider(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)
  1. 完成后的代码应如下所示:
// Create an instance of the ViewModel Factory.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

// Get a reference to the ViewModel associated with this fragment.
val sleepTrackerViewModel =
       ViewModelProvider(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)

下面是到目前为止的 onCreateView() 方法:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Get a reference to the binding object and inflate the fragment views.
        val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_tracker, container, false)

        val application = requireNotNull(this.activity).application

        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao

        val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

        val sleepTrackerViewModel =
                ViewModelProvider(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)

        return binding.root
    }

第 4 步:为视图模型添加数据绑定

在基本的 ViewModel 就绪之后,您需要在 SleepTrackerFragment 中完成数据绑定的设置,以将 ViewModel 与界面连接起来。

fragment_sleep_tracker.xml 布局文件中:

  1. <data> 部分内,创建一个引用 SleepTrackerViewModel 类的 <variable>
<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

SleepTrackerFragment.kt 中:

  1. 将当前的 activity 设置为绑定的生命周期所有者。在 onCreateView() 方法内添加以下代码,在 return 语句前面:
binding.setLifecycleOwner(this)
  1. sleepTrackerViewModel 绑定变量分配给 sleepTrackerViewModel。将以下代码放在 onCreateView() 内,在用于创建 SleepTrackerViewModel 的代码下面:
binding.sleepTrackerViewModel = sleepTrackerViewModel
  1. 您可能会看到一个错误,因为您必须重新创建绑定对象。请清除并重建项目以消除该错误。
  2. 最后,像往常一样,确保代码的构建和运行没有错误。

在 Kotlin 中,使用协程来顺畅且高效地处理长时间运行的任务,而不是回调。Kotlin 协程使您能够将基于回调的代码转换为顺序代码。顺序编写的代码通常更易于阅读和维护。与回调不同,协程可以安全地使用有价值的语言功能,如异常。最重要的是,协程具有更高程度的可维护性和灵活性。最后,协程和回调执行相同的功能:它们都能够处理应用中可能长时间运行的异步任务。

952b19bd8601a7a5.png

协程具有以下属性:

  • 协程是异步且非阻塞的。
  • 协程使用挂起函数来让异步代码依序调用。

协程是异步的。

协程独立于程序的主执行步骤运行。这可以是并行运行,也可以是在单独的处理器上运行。还有可能是当应用的其余部分正在等待输入时,您暗中执行少量的处理。异步编程的一个重要方面是,您不能期望结果立即可用,除非您明确等待结果。

例如,假设您有一个需要研究的问题,您礼貌地请求一位同事帮您找到答案。然后,您的同事就开始自己研究了。您可以继续做其他不依赖于该答案的无关工作,直到您的同事给您答案。在本例中,您的同事就是“在一个单独的线程上”异步完成工作。

协程是非阻塞的。

非阻塞意味着协程不会阻塞或干扰主线程或界面线程的进度。因此使用协程,用户可以获得尽可能流畅的体验,因为在主线程上运行的界面交互总是得到优先处理。

协程使用挂起函数来让异步代码依序调用。

关键字 suspend 是 Kotlin 将函数(或函数类型)标记为可供协程使用的一种方式。当协程调用标有 suspend 的函数时,它不会像常规函数调用一样在函数返回结果之前进行阻塞,而是挂起执行,直到结果就绪为止。然后,协程会从上次停止的位置恢复并使用返回的结果。

当协程挂起并等待结果时,它会取消阻塞正在运行它的线程。这样,其他函数或协程就可以运行了。

suspend 关键字未指定运行代码的线程。挂起函数可以在后台线程上运行,也可以在主线程上运行。

ce77d98e12909f3e.png

如需在 Kotlin 中使用协程,您需要以下三项:

  • 作业
  • 调度程序
  • 作用域

作业:基本上,作业是可以取消的任何内容。每个协程都有一项作业,您可以使用该作业来取消协程。您可以将作业安排到父子层次结构中。如果取消一项父作业,会立即取消该作业的所有子级,这样比手动取消每个协程要方便得多。

调度程序:调度程序发出协程以在各种线程上运行。例如,Dispatchers.Main 在主线程上运行任务,而 Dispatchers.IO 将阻塞的 I/O 任务分流到共享线程池。

作用域:协程的作用域定义协程在什么上下文中运行。作用域将有关协程的作业和调度程序的信息组合在一起。作用域会跟踪协程。当您启动一个协程时,它就会“在一个作用域中”,这意味着,您已指明哪个作用域将跟踪该协程。

Kotlin 协程与架构组件

CoroutineScope 会跟踪您的所有协程,并帮助您管理您的协程应在何时运行。它还可以取消在它之内启动的所有协程。每个异步操作或协程都在特定的 CoroutineScope 内运行。

架构组件针对应用中的逻辑作用域为协程提供了一流的支持。架构组件定义了以下内置作用域供您在应用中使用。这些内置协程作用域在每个相应架构组件的 KTX 扩展程序中。请务必在使用这些作用域时添加相应的依赖项。

ViewModelScope:为应用中的每个 ViewModel 定义了 ViewModelScope。如果清除了 ViewModel,则在此作用域中启动的任何协程都会自动取消。在此 Codelab 中,您将使用 ViewModelScope 来启动数据库操作。

Room 和调度程序

使用 Room 库执行数据库操作时,Room 将使用 Dispatchers.IO 在后台线程中执行数据库操作。您不必明确指定任何 Dispatchers。Room 将为您执行此操作。

您希望用户能够通过以下方式与睡眠数据进行交互:

  • 当用户点按 START 按钮时,应用会创建一个新的睡眠之夜,并将该睡眠之夜存储在数据库中。
  • 当用户点按 STOP 按钮时,应用会使用结束时间更新睡眠之夜。
  • 当用户点按 CLEAR 按钮时,应用会清空数据库中的数据。

这些数据库操作可能需要很长时间才能完成,因此应在单独的线程上运行。

第 1 步:将 DAO 函数标记为挂起函数

SleepDatabaseDao.kt 中,将便捷方法更改为挂起函数。

  1. 打开 database/SleepDatabaseDao.kt,将 suspend 关键字添加到除 getAllNights() 之外的所有方法,因为 Room 已经为该特定 @Query(返回 LiveData)使用后台线程。完整的 SleepDatabaseDao 类将如下所示。
@Dao
interface SleepDatabaseDao {

   @Insert
   suspend fun insert(night: SleepNight)

   @Update
   suspend fun update(night: SleepNight)

   @Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
   suspend fun get(key: Long): SleepNight?

   @Query("DELETE FROM daily_sleep_quality_table")
   suspend fun clear()

   @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
   suspend fun getTonight(): SleepNight?

   @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
   fun getAllNights(): LiveData<List<SleepNight>>
}

第 2 步:为数据库操作设置协程

当用户点按睡眠跟踪器应用中的 START 按钮时,您希望调用 SleepTrackerViewModel 中的函数来创建一个新的 SleepNight 实例,并将该实例存储在数据库中。

点按任意按钮都会触发数据库操作,如创建或更新 SleepNight。由于数据库操作可能需要一些时间才能完成,因此您使用协程来为应用的按钮实现点击处理程序。

  1. 打开应用级 build.gradle 文件。在依赖项部分下方,您需要我们已为您添加的以下依赖项。
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"

// Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
  1. 打开 SleepTrackerViewModel.kt 文件。
  2. 定义一个名为 tonight 的变量,用于保存当晚的数据。将该变量设为 MutableLiveData,因为您需要能够观察数据并更改数据。
private var tonight = MutableLiveData<SleepNight?>()
  1. 如需尽快初始化 tonight 变量,请在 tonight 的定义下面创建一个 init 代码块,并调用 initializeTonight()。您将在下一步中定义 initializeTonight()
init {
   initializeTonight()
}
  1. init 代码块下面,实现 initializeTonight()。使用 viewModelScope.launchViewModelScope 中启动一个协程。在大括号内,通过调用 getTonightFromDatabase() 从数据库中获取 tonight 的值,并将该值分配给 tonight.value。您将在下一步中定义 getTonightFromDatabase()

请注意,为 launch 使用了大括号。它们定义的是一个 lambda 表达式,这是一个没有名称的函数。在本例中,您将一个 lambda 传入 launch 协程构建器。此构建器会创建一个协程,并将该 lambda 的执行分配给相应的调度程序。

private fun initializeTonight() {
   viewModelScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. 实现 getTonightFromDatabase()。将其定义为一个 private suspend 函数,如果当前没有启动的 SleepNight,该函数将会返回可为 null 的 SleepNight。这样会产生错误,因为该函数必须返回一些内容。
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. getTonightFromDatabase() 的函数主体内,从数据库中获取 tonight(最新的睡眠之夜)。如果开始时间和结束时间不同,表示该睡眠之夜已完成,返回 null。否则,返回该睡眠之夜。
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       return night

完成后的 getTonightFromDatabase() suspend 函数应如下所示。应该不会再有错误了。

private suspend fun getTonightFromDatabase(): SleepNight? {
    var night = database.getTonight()
    if (night?.endTimeMilli != night?.startTimeMilli) {
        night = null
    }
    return night
}

第 3 步:添加“START”按钮的点击处理程序

现在,您可以实现 onStartTracking(),它是 START 按钮的点击处理程序。您需要创建一个新的 SleepNight,将其插入数据库,并将其分配给 tonightonStartTracking() 的结构将与 initializeTonight() 类似。

  1. SleepTrackerViewModel.kt 中,从 onStartTracking() 的函数定义开始。您可以将点击处理程序放在 getTonightFromDatabase() 下面。
fun onStartTracking() {}
  1. onStartTracking() 内,在 viewModelScope 中启动一个协程,因为您需要此结果才能继续操作并更新界面。
viewModelScope.launch {}
  1. 在用于启动协程的代码内,创建一个新的 SleepNight,它将捕获当前时间作为开始时间。
        val newNight = SleepNight()
  1. 仍然在用于启动协程的代码内,调用 insert() 以将 newNight 插入数据库。您将会看到一个错误,因为您还没有定义此 insert() 挂起函数。请注意,这与 SleepDatabaseDAO.kt 中的同名 insert() 方法不同。
       insert(newNight)
  1. 还是在用于启动协程的代码内,更新 tonight
       tonight.value = getTonightFromDatabase()
  1. onStartTracking() 下面,将 insert() 定义为一个 private suspend 函数,该函数将 SleepNight 作为其参数。
private suspend fun insert(night: SleepNight) {}
  1. insert() 方法中,使用 DAO 将睡眠之夜插入数据库。
       database.insert(night)

请注意,一个带有 Room 的协程使用 Dispatchers.IO,所以这不会发生在主线程上。

  1. fragment_sleep_tracker.xml 布局文件中,使用您之前设置的数据绑定的强大功能将 onStartTracking() 的点击处理程序添加到 start_button@{() -> 函数符号会创建一个 lambda 函数,该函数不采用任何参数,并在 sleepTrackerViewModel 中调用点击处理程序。
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. 构建并运行您的应用。点按 START 按钮。此操作会创建数据,但您还看不到任何内容。您接下来就会解决该问题。

不使用 Room

fun someWorkNeedsToBeDone {
   viewModelScope.launch {
        suspendFunction()
   }
}

suspend fun suspendFunction() {
   withContext(Dispatchers.IO) {
       longrunningWork()
   }
}

使用 Room

// Using Room
fun someWorkNeedsToBeDone {
   viewModelScope.launch {
        suspendDAOFunction()
   }
}

suspend fun suspendDAOFunction() {
   // No need to specify the Dispatcher, Room uses Dispatchers.IO.
   longrunningDatabaseWork()
}

第 4 步:显示数据

SleepTrackerViewModel.kt 中,nights 变量引用 LiveData,因为 DAO 中的 getAllNights() 会返回 LiveData

每当数据库中的数据发生变化时,LiveData nights 都会更新以显示最新数据,这是一项 Room 功能。您从不需要明确设置 LiveData 或对其进行更新。Room 会更新数据以与数据库匹配。

不过,如果在文本视图中显示 nights,将显示对象引用。如需查看对象的内容,请将数据转换为设置了格式的字符串。使用每当 nights 从数据库接收新数据时都会执行的 Transformation 映射。

  1. 打开 Util.kt 文件,然后取消备注 formatNights() 的定义和关联的 import 语句的代码。如需在 Android Studio 中取消备注代码,请选择标有 // 的所有代码,然后按 Cmd+/Control+/
  2. 请注意,formatNights() 返回一个类型 Spanned,它是 HTML 格式的字符串。这非常方便,因为 Android 的 TextView 能够渲染基本 HTML。
  3. 依次打开 res > values > strings.xml。请注意,使用了 CDATA 来设置字符串资源的格式以显示睡眠数据。
  4. 打开 SleepTrackerViewModel.kt。在 SleepTrackerViewModel 类中,定义一个名为 nights 的变量。从数据库中获取所有睡眠之夜,并将其分配给 nights 变量。
private val nights = database.getAllNights()
  1. 就在 nights 的定义下面,添加相应的代码以将 nights 转换为 nightsString。使用 Util.kt 中的 formatNights() 函数。

nights 传入 Transformations 类中的 map() 函数。如需访问字符串资源,请将映射函数定义为发起调用的 formatNights()。提供 nightsResources 对象。

val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. 打开 fragment_sleep_tracker.xml 布局文件。在 TextView 内的 android:text 属性中,您现在可以将资源字符串替换为对 nightsString 的引用。
android:text="@{sleepTrackerViewModel.nightsString}"
  1. 重建代码并运行应用。现在应显示您的所有睡眠数据与开始时间。
  2. 再点按几次 START 按钮,您会看到更多数据。e6eabf9793d5ab63.png

在下一步中,您将启用 STOP 按钮的功能。

第 5 步:添加“STOP”按钮的点击处理程序

使用与上一步中相同的模式,在 SleepTrackerViewModel. 中实现 STOP 按钮的点击处理程序。

  1. onStopTracking() 添加到 ViewModel。在 viewModelScope 中启动一个协程。如果尚未设置结束时间,请将 endTimeMilli 设置为当前系统时间,并使用睡眠之夜数据调用 update()

在 Kotlin 中,return@label 语法指定此语句从几个嵌套函数中的哪个函数返回结果。

fun onStopTracking() {
   viewModelScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. 实现 update(),使用的模式与用于实现 insert() 的模式相同。
private suspend fun update(night: SleepNight) {
    database.update(night)
}
  1. 如需将点击处理程序连接到界面,请打开 fragment_sleep_tracker.xml 布局文件,然后向 stop_button 添加点击处理程序。
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. 构建并运行您的应用。
  2. 点按 START,然后点按 STOP。您会看到开始时间、结束时间、没有值的睡眠质量和睡眠时长。fc7e561d95fd9c02.png

第 6 步:添加“CLEAR”按钮的点击处理程序

  1. 同样,实现 onClear()clear()
fun onClear() {
   viewModelScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
    database.clear()
}
  1. 如需将点击处理程序连接到界面,请打开 fragment_sleep_tracker.xml,然后向 clear_button 添加点击处理程序。
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
  1. 构建并运行您的应用。
  2. 点按 CLEAR 以清除所有数据。然后,点按 STARTSTOP 以生成新数据。

Android Studio 项目:TrackMySleepQualityCoroutines

  • 使用 ViewModelViewModelFactory 和数据绑定来设置应用的界面架构。
  • 为使界面顺畅运行,使用协程来执行长时间运行的任务,如所有数据库操作。
  • 协程是异步且非阻塞的。它们使用 suspend 函数来让异步代码依序调用。
  • 当协程调用标有 suspend 的函数时,它不会像常规函数调用一样在该函数返回结果之前进行阻塞,而是挂起执行,直到结果就绪为止。然后,它会从上次停止的位置恢复并使用返回的结果。
  • 阻塞与挂起的区别在于,如果线程被阻塞,不会进行其他工作。如果线程被挂起,会进行其他工作,直到结果可用为止。

如需实现触发数据库操作的点击处理程序,请遵循以下模式:

  1. 启动一个在主线程或界面线程上运行的协程,因为结果会影响界面。
  2. 调用一个挂起函数来完成长时间运行的工作,这样在等待结果时就不会阻塞界面线程。
  3. 长时间运行的工作与界面无关,因此切换到 I/O 上下文。这样,工作就可以在针对这些类型的操作进行了优化并为其预留的线程池中运行。
  4. 然后,调用长时间运行的函数来完成工作。

每当 LiveData 对象发生变化时,都使用 Transformations 映射从该对象创建一个字符串。

Udacity 课程:

Android 开发者文档:

其他文档和文章:

此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:

  • 根据需要布置作业。
  • 告知学生如何提交家庭作业。
  • 给家庭作业评分。

讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。

如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。

回答以下问题

问题 1

以下哪一项不是使用协程的优势?

  • 它们是非阻塞的。
  • 它们异步运行。
  • 它们可以在除主线程以外的线程上运行。
  • 它们总是能够让应用更快地运行。
  • 它们可以使用异常。
  • 它们可以作为线性代码进行写入和读取。

问题 2

在以下关于挂起函数的表述中,哪一项是不正确的?

  • 使用 suspend 关键字进行注解的普通函数。
  • 可以在协程内调用的函数。
  • 当挂起函数正在运行时,发起调用的线程会挂起。
  • 挂起函数必须始终在后台运行。

问题 3

以下哪一项表述是不正确的?

  • 执行阻塞后,无法在阻塞的线程上执行任何其他工作。
  • 执行挂起后,线程可以在等待分流的工作完成的同时执行其他工作。
  • 挂起更高效,因为线程可能不会等待,什么也不做。
  • 无论执行阻塞还是挂起,都仍会等待返回协程结果,然后再继续。

开始学习下一课:使用 LiveData 控制按钮状态

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