上一篇介绍了将 线程 转向到 使用 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) 调用方不必将回调传递给此函数。
它们可以依赖挂起和恢复来获取结果或错误。
再次运行应用,就会看到基于协程的新实现会从网络加载结果!