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

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

简介

为应用打造完美的用户体验,首要任务之一是确保界面始终具备自适应能力,并且能够顺畅运行。提高界面性能的一种方法是将长时间运行的任务(例如数据库操作)移到后台。

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

您应当已掌握的内容

您应熟悉以下内容/操作:

  • 使用 Activity、Fragment、View 和点击处理程序构建基本界面 (UI)。
  • 在 fragment 之间导航,并使用 safeArgs 在 fragment 之间传递简单的数据。
  • 查看模型、查看模型工厂、转换和 LiveData
  • 如何创建 Room 数据库、创建 DAO 以及定义实体。
  • 如果您熟悉线程处理和多处理概念,将会很有帮助。

学习内容

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

您将执行的操作

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

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

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

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

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

用户的流程如下所示:

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

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

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

在此任务中,您将使用 TextView 显示设置了格式的睡眠跟踪数据。(这不是最终接口。您将在另一个 Codelab 中学习更好的方法。)

您可以继续使用在上一个 Codelab 中构建的 TrackMySleepQuality 应用,或下载此 Codelab 的入门应用

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

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

第 2 步:检查代码

本 Codelab 的起始代码与 6.1 创建 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 以查看其 XML 布局。请注意以下事项:
  • 布局数据封装在 <layout> 元素中,以启用数据绑定。
  • ConstraintLayout 及其他视图被安排在 <layout> 元素内。
  • 此文件有一个占位符 <data> 标记。

起始应用还提供了界面的尺寸、颜色和样式设置。该应用包含 Room 数据库、DAO 和 SleepNight 实体。如果您未学完上述 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 中,获取对应用上下文的引用。将该引用放在 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 =
       ViewModelProviders.of(
               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 =
       ViewModelProviders.of(
               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 =
                ViewModelProviders.of(
                        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 中:

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

在 Kotlin 中,协程是优雅高效地处理长时间运行的任务的方式。Kotlin 协程使您能够将基于回调的代码转换为顺序的代码。依序编写的代码通常更易于阅读,甚至可以使用异常语言等语言功能。最后,协程和回调会执行相同的操作:它们会等待长时间运行的任务获得结果,然后继续执行。

协程具有以下属性:

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

协程是异步的。

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

例如,假设您需要研究某个问题,您需要请同事找出答案。他们负责处理工作,就好像他们是在异步工作“异步”工作,在单独的线程上处理工作。您可以继续执行其他操作,但不依赖于答案,直到您的同事回来告诉您答案。

协程是非阻塞的。

非阻塞意味着协程不会阻塞主线程或界面线程。因此,使用协程时,用户始终享有最流畅的体验,因为界面交互始终优先。

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

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

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

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

如需在 Kotlin 中使用协程,您需要具备三项条件:

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

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

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

作用域:协程的作用域定义了协程的运行环境。范围结合了有关协程作业和调度程序的信息。作用域会跟踪协程。当您启动一个协程时,它就会“在一个作用域中”,这意味着,您已指明哪个作用域将跟踪该协程。

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

  • 在用户点按 Start 按钮后,应用会创建新的睡眠晚并将其存储在数据库中。
  • 当用户点按停止按钮时,应用会更新结束时间。
  • 当用户点按 CLEAR 按钮时,应用会清空数据库中的数据。

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

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

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

点按任意按钮都会触发数据库操作,如创建或更新 SleepNight。因此,您可以使用协程为应用按钮实现点击处理程序。

  1. 打开应用级 build.gradle 文件,并查找协程的依赖项。如需使用协程,您需要添加这些依赖项。

    $coroutine_version 在项目 build.gradle 文件中定义为 coroutine_version = '1.0.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
  1. 打开 SleepTrackerViewModel 文件。
  2. 在类的正文中,定义 viewModelJob 并为其分配实例 Job。借助此 viewModelJob,您可以在视图模型不再使用且被销毁时取消此视图模型启动的所有协程。这样,您就不会最终无处返回到协程。
private var viewModelJob = Job()
  1. 在类正文的末尾,替换 onCleared() 并取消所有协程。当 ViewModel 被销毁时,系统会调用 onCleared()
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. viewModelJob 定义的正下方,为协程定义 uiScope。范围决定了协程将在哪个线程上运行,并且范围也需要了解该作业。如需获取范围,请请求 CoroutineScope 的实例,并传入调度程序和作业。

使用 Dispatchers.Main 意味着在 uiScope 中启动的协程将在主线程上运行。这对于由 ViewModel 启动的许多协程来说是合理的,因为在这些协程执行一些处理后,它们会更新界面。

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
  1. uiScope 的定义下,定义一个名为 tonight 的变量以存储当前夜晚。将变量设置为 MutableLiveData,因为您需要能够观察和更改数据。
private var tonight = MutableLiveData<SleepNight?>()
  1. 如需尽快初始化 tonight 变量,请在 tonight 的定义下面创建一个 init 代码块,并调用 initializeTonight()。您将在下一步中定义 initializeTonight()
init {
   initializeTonight()
}
  1. init 代码块下面,实现 initializeTonight()。在 uiScope 中,启动协程。在函数内,通过调用 getTonightFromDatabase() 从数据库中获取 tonight 的值,并将值分配给 tonight.value。您将在下一步中定义 getTonightFromDatabase()
private fun initializeTonight() {
   uiScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. 实现 getTonightFromDatabase()。将其定义为一个 private suspend 函数,如果当前没有启动的 SleepNight,该函数将会返回可为 null 的 SleepNight。这会留下错误,因为函数必须返回一些内容。
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. getTonightFromDatabase() 的函数正文内,从在 Dispatchers.IO 上下文中运行的协程返回结果。使用 I/O 调度程序,因为从数据库获取数据属于 I/O 操作,与界面无关。
  return withContext(Dispatchers.IO) {}
  1. 在返回块内,让协程在今晚(最近的一晚)从数据库中获取。如果开始时间和结束时间不相同(即夜晚已完成),则返回 null。否则,请过夜。
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night

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

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

第 2 步:为“开始”按钮添加点击处理程序

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

  1. onStartTracking() 的函数定义开始。您可以将点击处理程序放在 SleepTrackerViewModel 文件中的 onCleared() 上方。
fun onStartTracking() {}
  1. onStartTracking() 中,在 uiScope 中启动协程,因为您需要此结果才能继续和更新界面。
uiScope.launch {}
  1. 在用于启动协程的代码内,创建一个新的 SleepNight,它将捕获当前时间作为开始时间。
        val newNight = SleepNight()
  1. 仍然在用于启动协程的代码内,调用 insert() 以将 newNight 插入数据库。您将会看到一个错误,因为您还没有定义此 insert() 挂起函数。(这不是同名的 DAO 函数。)
       insert(newNight)
  1. 还是在用于启动协程的代码内,更新 tonight
       tonight.value = getTonightFromDatabase()
  1. onStartTracking() 下,将 insert() 定义为接受 SleepNight 作为其参数的 private suspend 函数。
private suspend fun insert(night: SleepNight) {}
  1. 对于 insert() 的正文,在 I/O 上下文中启动协程,并通过从 DAO 调用 insert() 将夜间插入到数据库中。
   withContext(Dispatchers.IO) {
       database.insert(night)
   }
  1. fragment_sleep_tracker.xml 布局文件中,使用您之前设置的数据绑定的强大功能将 onStartTracking() 的点击处理程序添加到 start_button@{() -> 函数符号会创建一个 lambda 函数,该函数不采用任何参数,并在 sleepTrackerViewModel 中调用点击处理程序。
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. 构建并运行您的应用。点按 START 按钮。此操作会创建数据,但您还看不到任何内容。接下来您将解决这一问题。
fun someWorkNeedsToBeDone {
   uiScope.launch {

        suspendFunction()

   }
}

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

第 3 步:显示数据

SleepTrackerViewModel 中,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 格式的字符串。
  3. 打开 strings.xml。请注意使用 CDATA 设置用于显示睡眠数据的字符串资源的格式。
  4. 打开 SleepTrackerViewModel。在 SleepTrackerViewModel 类中的 uiScope 定义下,定义一个名为 nights 的变量。从数据库中获取整晚,并将其分配给 nights 变量。
private val nights = database.getAllNights()
  1. 就在 nights 的定义下面,添加相应的代码以将 nights 转换为 nightsString。使用 Util.kt 中的 formatNights() 函数。

    nightsTransformations 类传入 map() 函数。如需访问字符串资源,请将映射函数定义为调用 formatNights()。提供 nightsResources 对象。
val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. 打开 fragment_sleep_tracker.xml 布局文件。在 TextView 中的 android:text 属性中,您现在可以将资源字符串替换为对 nightsString 的引用。
"@{sleepTrackerViewModel.nightsString}"
  1. 重新构建代码并运行应用。现在将显示包含开始时间的所有睡眠数据。
  2. 点按开始按钮几次,系统就会显示更多数据。

在下一步中,您将为 Stop 按钮启用功能。

第 4 步:为“停止”按钮添加点击处理程序

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

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

    在 Kotlin 中,return@label 语法会指定此嵌套函数从多个嵌套函数中返回的函数。
fun onStopTracking() {
   uiScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. 使用与实现 insert() 相同的模式实现 update()
private suspend fun update(night: SleepNight) {
   withContext(Dispatchers.IO) {
       database.update(night)
   }
}
  1. 如需将点击处理程序连接到界面,请打开 fragment_sleep_tracker.xml 布局文件,然后向 stop_button 添加点击处理程序。
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. 构建并运行您的应用。
  2. 点按 START,然后点按 STOP。您会看到开始时间、结束时间、无价值的睡眠质量,以及睡眠时间。

第 5 步:为“清除”按钮添加点击处理程序

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

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

Android Studio 项目:TrackMySleepQualityCoroutines

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

如需启动协程,您需要一个作业、一个调度程序和一个作用域:

  • 基本上,作业是可以取消的任何内容。每个协程都有一个作业,您可以使用作业取消协程。
  • 调度程序会发送要在各种线程上运行的协程。Dispatcher.Main 在主线程上运行任务,Dispartcher.IO 用于将阻塞的 I/O 任务分流到共享线程池。
  • 范围结合信息(包括任务和调度程序)来定义运行协程的上下文。作用域会跟踪协程。

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

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

每次改变对象时,都请使用 Transformations 地图从 LiveData 对象创建一个字符串。

Udacity 课程:

Android 开发者文档:

其他文档和文章:

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

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

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

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

回答这些问题

问题 1

以下哪些是协程的优势:

  • 它们是非阻塞的
  • 它们异步运行。
  • 它们可以在除主线程以外的线程上运行。
  • 它们始终能够提高应用的运行速度。
  • 它们可以使用异常。
  • 它们可以作为线性代码进行写入和读取。

问题 2

什么是挂起函数?

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

问题 3

阻塞和挂起线程有何区别?请标记所有正确的值。

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

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

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