1、从阻塞代码创建主线程安全函数
通过这一章节,您将学习如何切换运行协程,以实现TitleRepository
的工作版本。
1.1、查看refreshTitle中的现有回调代码
打开TitleRepository.kt
并查看现有的基于回调的实现。 TitleRepository.kt
kotlin
// 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))
}
}
}
在TitleRepository.kt
中,refreshTitleWithCallbacks
方法通过回调来实现,以便将加载和错误状态传达给调用放。
为了实现刷新,此函数会执行多项操作。
- 切换到包含
BACKGROUND``ExecutorService
的另一个线程。 - 使用阻塞
execute()
方法运行fetchNextTitle
网络请求。这将在当前线程中运行网络请求,在本例中为BACKGROUND
中的一个线程。 - 如果结果成功,则使用
insertTitle
将其保存到数据库,并调用onCompleted()
方法。 - 如果结果不成功或者出现异常,则调用onError方法,以告知调用方刷新失败。
这种基于回调的时间是主线程安全 的,因为它不会阻塞主线程。但是,它必须在工作完成后使用回调来通知调用方。此外,它还会再它也已切换的BACKGROUND
线程上调用回调。
1.2、从协程调用阻塞调用
在不向网络或数据库引入协程的情况下,我们可以使用协程让次代码具有主线程安全性。这样,我们就可以移除回调,并将结果传回醉蛛调用回调的线程。
如果您需要再协程内执行阻塞或CPU密集型工作,例如排序和过滤大型列表或从磁盘读取数据,则可以使用此模式。
此模式应该用于与您代码中的阻塞API集成或执行CPU密集型工作。最好尽可能使用Room或Retrofit等库中的常规挂起函数。
在任何调度程序之间切换时,协程会使用withContext
。调用withContext
会切换到仅适用于lambda的另一个调度程序,然后返回到使用该lambda的结果调用它的调度程序。
Kotlin协程默认提供三个调度程序:Main
、IO
和Default
。IO调用程序针对IO工作进行了优化,例如从网络或磁盘读取内容,而Default调度程序则针对CPU密集型任务进行了优化。
TitleRepository.kt
kotlin
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 throw 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完成为止。
与回调版本相比,有一下两个主要区别:
withContext
将其结果返回给调用它的调度程序,在本例中调度程序为Dispatchers.Main
。回调版本在BACKGROUND
执行程序服务中的线程上调用回调。- 调用方不必将回调传递给此函数。它们可以依赖挂起和恢复来获取结果或错误。
2、Room和Retrofit中的协程
为了继续实现协程集成,我们将利用对稳定版Room和Retrofit中的挂起函数的支持,然后使用挂起函数大幅简化我们刚刚编写的代码。
2.1、Room中的协程
首先,打开MainDatabase.kt
并将insertTitle
设置为挂起函数: MainDatabase.kt
kotlin
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OneConflictStrategy.REPLACE)
suspend fun insertTItle(title: Title)
执行此操作后,Room会让您的检查具有主线程安全性,并自动在后台线程上执行此查询。不过,这也意味着您只能从协程内调用此查询。
以上就是在Room中使用协程所需执行的全部操作。
2.2、Refrefit中的协程
接下来,我们来看看如何将协程与Refrofit集成。打开MainNetwork.kt
并将fetchNextTitle
更改为挂起函数。此外,将返回值类型从Call<String>
更改为String
。
挂起函数支持需要Retrofit 2.6.0或更高版本
MainNetWork.kt
kotlin
// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String
interface MainNetwork {
@Get("next_title.json")
suspend fun fetchNextTitle(): String
}
要将挂起函数与Retrofit一起使用,您必须执行以下两项操作:
- 为函数添加挂起修饰符
- 从返回值类型中移除
Call
封装容器。这里我们会返回String
,但您可以可返回json支持的复杂类型。如果您仍希望提供对Retrofit的完整Result
的访问权限,您可以从挂起函数返回Result<String>
而不是String
。
Retrofit将会自动使挂起函数具有主线程安全性 ,以便您可以直接从Dispatchers.Main
调用它们。
sql
Room和Retrofit均会让挂起函数具有主线程安全性。
尽管这些挂起函数从网络中提取数据并将数据写入数据库,您可以安全地从Dispatchers.Main调用这些函数。
Room和Retrofit都使用自定义调度程序,而不使用Dispatchers.IO
Room会使用已配置的默认查询和事务Executor运行协程。
Retrofit将在后台创建新的Call对象,并将其调用队列以一步发送请求。
2.3、使用Room和Retrofit
现在,Room和Retrofit支持挂起函数,因此我们可以从代码库中使用它们。打开TitleRepository.kt
,并观察使用挂起函数如何大大简化逻辑,甚至与苏泽版本相比也不例外:
TitleRepository.kt
kotlin
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
哇,这样要短得多。可这是怎么回事?事实证明,依赖挂起和恢复操作会使代码大幅缩短。借助Retrofit,我们可以在此处使用String
或User
对象等返回值类型,而不是Call
。这样做石泉县的,因为在挂起函数内,Retrofit
能够在后台线程上运行网络请求,并在调用完成时恢复协程。
更棒的是,我们去掉了withContext
。由于Room和Retrofit都提供主线程安全 挂起函数,因此可以安全地通过Dispatchers.Main
安排此异步工作。
bash
您需要使用withContext来调用主线程安全挂起函数。
按照惯例,您应确保在应用中编写的suspend函数具有主线程安全性。这样一来,您便可安全地从任意调度程序(甚至是Dispacthers.Main)调用这些函数。
2.4、修正编译器错误
专用协程确实设计更改函数的签名,因为您无法通过常规函数调用挂起函数。如果您在此步骤中添加了suspend
修饰符,系统会生成一些编译器错误,从中您会明白在实际项目中奖函数更改为挂起函数时会发生的情况。
检查醒目,并修正将函数更改为挂起函数时所产生的编辑器错误。以下是快速解决各类问题的方法:
2.4.1、TestingFakes.kt
更新测试虚假对象,以支持新的挂起修饰符。
TitleDaoFake
- 按Alt + Enter(在Mac上按Option + Enter),将挂起修饰符添加到层次结果中的所有函数。
MainNetworkFake
- 按Alt + Enter(在Mac上按Option + Enter),将挂起修饰符添加到层级结构中的所有函数。
- 将
fetchNextTitle
替换为此函数
kotlin
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- 按Alt + Enter(在Mac上按Option + Enter),将挂起修饰符添加到层级结构中的所有函数。
- 将
fetchNextTitle
替换为此函数
kotlin
override suspend fun fetchNextTiel() = completable.await()
TitleRepository.kt
- 删除
refreshTitleWithCallbacks
函数,因为系统已不再使用它。
2.5、运行应用
再次运行应用,编译完成后,您会发现它使用协程将数据从ViewModel一直加载到Room和Retrofit!