Kotlin之协程(第五趴)——从阻塞代码创建主线程安全函数&Room和Retrofit中的协程

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方法通过回调来实现,以便将加载和错误状态传达给调用放。

为了实现刷新,此函数会执行多项操作。

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

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

1.2、从协程调用阻塞调用

在不向网络或数据库引入协程的情况下,我们可以使用协程让次代码具有主线程安全性。这样,我们就可以移除回调,并将结果传回醉蛛调用回调的线程。

如果您需要再协程内执行阻塞或CPU密集型工作,例如排序和过滤大型列表或从磁盘读取数据,则可以使用此模式。

此模式应该用于与您代码中的阻塞API集成或执行CPU密集型工作。最好尽可能使用Room或Retrofit等库中的常规挂起函数。

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

Kotlin协程默认提供三个调度程序:MainIODefault。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上运行)会挂起,直到withContextlambda完成为止。

与回调版本相比,有一下两个主要区别:

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

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一起使用,您必须执行以下两项操作:

  1. 为函数添加挂起修饰符
  2. 从返回值类型中移除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,我们可以在此处使用StringUser对象等返回值类型,而不是Call。这样做石泉县的,因为在挂起函数内,Retrofit能够在后台线程上运行网络请求,并在调用完成时恢复协程。

更棒的是,我们去掉了withContext。由于Room和Retrofit都提供主线程安全 挂起函数,因此可以安全地通过Dispatchers.Main安排此异步工作。

bash 复制代码
您需要使用withContext来调用主线程安全挂起函数。

按照惯例,您应确保在应用中编写的suspend函数具有主线程安全性。这样一来,您便可安全地从任意调度程序(甚至是Dispacthers.Main)调用这些函数。

2.4、修正编译器错误

专用协程确实设计更改函数的签名,因为您无法通过常规函数调用挂起函数。如果您在此步骤中添加了suspend修饰符,系统会生成一些编译器错误,从中您会明白在实际项目中奖函数更改为挂起函数时会发生的情况。

检查醒目,并修正将函数更改为挂起函数时所产生的编辑器错误。以下是快速解决各类问题的方法:

2.4.1、TestingFakes.kt

更新测试虚假对象,以支持新的挂起修饰符。

TitleDaoFake

  1. 按Alt + Enter(在Mac上按Option + Enter),将挂起修饰符添加到层次结果中的所有函数。

MainNetworkFake

  1. 按Alt + Enter(在Mac上按Option + Enter),将挂起修饰符添加到层级结构中的所有函数。
  2. fetchNextTitle替换为此函数
kotlin 复制代码
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. 按Alt + Enter(在Mac上按Option + Enter),将挂起修饰符添加到层级结构中的所有函数。
  2. fetchNextTitle替换为此函数
kotlin 复制代码
override suspend fun fetchNextTiel() = completable.await()

TitleRepository.kt

  • 删除refreshTitleWithCallbacks函数,因为系统已不再使用它。

2.5、运行应用

再次运行应用,编译完成后,您会发现它使用协程将数据从ViewModel一直加载到Room和Retrofit!

相关推荐
工业甲酰苯胺1 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3431 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee2 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯3 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey4 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!6 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟7 小时前
Android音频采集
android·音视频
小白也想学C8 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程8 小时前
初级数据结构——树
android·java·数据结构
闲暇部落10 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin