在 Android 应用中使用 Kotlin 协程 - 官方示例详解(2)

上一篇介绍了将 线程 转向到 使用 Kotlin 的协程 以及 如何测试 协程.
https://www.jianshu.com/p/42464606fe08

本篇将介绍将 回调 转向 协程, 以及 创建主线程 安全函数.

前言

在将架构的各个部分转换为使用协程之前,最好先了解每个部分的作用。

(1)MainDatabase 使用 Room 实现一个数据库,以保存和加载 Title。
(2)MainNetwork 实现一个网络 API,用于提取新标题。它使用 Retrofit 提取标题。
Retrofit 配置为随机返回错误或模拟数据,但除此之外其行为就像是在发出实际网络请求一样。
(3)TitleRepository 实现了一个 API,用于通过结合来自网络和数据库的数据来提取或刷新标题。
(4)MainViewModel 表示屏幕的状态,并负责处理事件。它会指示代码库在用户点按屏幕时刷新标题

由于 网络请求 由 界面事件驱动,并且我们希望根据这些事件启动协程,
那么自然而然应在 ViewModel 中开始使用协程。

1 从回调转向协程。

1.1 回调版本

打开 MainViewModel.kt 可查看 refreshTitle 的声明。

/**
* Update title text via this LiveData
*/
val title = repository.title

// ... other code ...

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   // TODO: Convert refreshTitle to use coroutines
   _spinner.value = true
   repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
       override fun onCompleted() {
           _spinner.postValue(false)
       }

       override fun onError(cause: Throwable) {
           _snackBar.postValue(cause.message)
           _spinner.postValue(false)
       }
   })
}

每次用户点击屏幕时,系统都会调用此函数,这会导致代码库刷新标题,然后将新标题写入数据库

此实现使用回调来执行几项操作:
(1)在开始查询之前,它使用 _spinner.value = true 显示一个加载旋转图标
(2)当获得结果时,它使用 _spinner.value = false 清除加载旋转图标
(3)如果出现错误,它会指示系统显示信息提示控件并清除旋转图标

请注意,系统不会向 onCompleted 回调函数传递 title。
由于我们将所有标题写入 Room 数据库,
因此界面通过观察由 Room 更新的 LiveData 来更新为最新标题。

1.2 协程版本

1.2.1 TitleRepository.kt

创建挂起函数, 则它可以与协程配合使用.

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

现在,它会等待 500 毫秒来假装在执行操作,然后再继续
实际这里需要用 Retrofit 和 Room 提取新标题,并使用协程将标题写入数据库。

1.2.2 MainViewModel.kt

将 refreshTitle 的回调版本替换为启动新协程的版本:

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true //(a)
           repository.refreshTitle() //(b)
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

代码分析:
(1) viewModelScope.launch 表示在 viewModelScope 中启动一个新的协程.
默认是指定 Dispatchers.Main。
尽管 refreshTitle 会发出网络请求和数据库查询,但它可以使用协程公开主线程安全接口。
所以可以安全地从主线程调用它。
(2) 由于我们使用了 viewModelScope,因此,当用户离开此屏幕时,此协程启动的操作将自动取消。
这意味着它不会发出其他网络请求或数据库查询。
(3) 代码(a) 中会启动加载旋转图标
(4) 代码(b) 中调用的是挂起函数。
这里不需要传递回调。 协程将挂起,直到 refreshTitle 恢复它为止。
协程看起来就像常规的阻塞函数调用一样,但它会自动等待网络和数据库查询完成,
然后才会恢复,不会阻塞主线程。
(5) 挂起函数中的异常的作用类似于常规函数中的错误.
可以使用常规 try/catch 块来处理.
如果从协程丢出异常,则此协程将默认取消其父级。也就是说,同时取消多项相关任务非常容易。
(6) 在一个 finally 块中,我们可以确保旋转图标始终在查询运行后关闭。

注意:viewModelScope.launch 启动的是在主线程??
因此 可以使用 _spinner.value 而不需要 _spinner.post ?

选择 start 配置并点按 png 再次运行应用,
会在点按任意位置时看到加载旋转图标。
由于我们尚未连接网络或数据库,标题不会发生变化。

2 创建主线程安全函数 (withContext)

2.1现有回调代码:

TitleRepository.kt

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
   // This request will be run on a background thread by retrofit
   BACKGROUND.submit {
       try {
           // Make network request using a blocking call
           val result = network.fetchNextTitle().execute()
           if (result.isSuccessful) {
               // Save it to database
               titleDao.insertTitle(Title(result.body()!!))
               // Inform the caller the refresh is completed
               titleRefreshCallback.onCompleted()
           } else {
               // If it's not successful, inform the callback of the error
               titleRefreshCallback.onError(
                       TitleRefreshError("Unable to refresh title", null))
           }
       } catch (cause: Throwable) {
           // If anything throws an exception, inform the caller
           titleRefreshCallback.onError(
                   TitleRefreshError("Unable to refresh title", cause))
       }
   }
}

该方法通过回调来实现,以便将加载和错误状态传达给调用方。

为了实现刷新,此函数会执行多项操作。
(1) 切换到包含 BACKGROUND ExecutorService 的另一个线程
(2) 使用阻塞 execute() 方法运行 fetchNextTitle 网络请求。
这将在当前线程中运行网络请求,在本例中为 BACKGROUND 中的一个线程。
(3) 如果结果成功,则使用 insertTitle 将其保存到数据库,并调用 onCompleted() 方法。
(4) 如果结果不成功或者出现异常,则调用 onError 方法,以告知调用方刷新失败。

这种基于回调的实现是主线程安全的,因为它不会阻塞主线程
但是,它必须在工作完成后使用 回调通知调用方。
此外,它还会在它也已切换的 BACKGROUND 线程上调用回调。

2.2 协程版本:

任何调度程序之间切换时,协程会使用 withContext。
调用 withContext 会切换到仅适用于 lambda 的另一个调度程序,
然后返回到使用该 lambda 的结果调用它的调度程序。

Kotlin 协程默认提供三个调度程序:Main、IO 和 Default。
IO 调度程序针对 IO 工作进行了优化,
例如从网络或磁盘读取内容,而 Default 调度程序则针对 CPU 密集型任务进行了优化。

协程代码:

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }

       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

此代码仍使用阻塞调用
调用 execute() 和 insertTitle(...) 都会阻塞正在运行此协程的线程
不过,通过使用 withContext 切换到 Dispatchers.IO,我们将阻塞 IO 调度程序中的某个线程。
调用此函数的协程(可能在Dispatchers.Main 上运行)会挂起,
直到 withContext lambda 完成为止。

与回调版本相比,有以下两个主要区别:
(1) withContext 将其结果返回给调用它的调度程序,在本例中调度程序为 Dispatchers.Main。
回调版本在 BACKGROUND 执行程序服务中的线程上调用回调。
(2) 调用方不必将回调传递给此函数。
它们可以依赖挂起和恢复来获取结果或错误。

再次运行应用,就会看到基于协程的新实现会从网络加载结果!

你可能感兴趣的:(在 Android 应用中使用 Kotlin 协程 - 官方示例详解(2))