使用协程和 Flow 简化 API 设计

检查现有协程适配器

在您为现有 API 编写自己的封装之前,请检查是否已经存在针对您的用例的适配器或者 扩展方法。下面是一些包含常见类型协程适配器的库。

Future 类型

对于 future 类型,Java 8 集成了 CompletableFuture,而 Guava 集成了 ListenableFuture。这里提到的并不是全部,您可以在线搜索以确定是否存在适用于您的 future 类型的适配器。

// 等待 CompletionStage 的执行完成而不阻塞线程
suspend fun CompletionStage.await(): T

// 等待 ListenableFuture 的执行完成而不阻塞线程
suspend fun ListenableFuture.await(): T

使用这些函数,您可以摆脱回调并挂起协程直到 future 的结果被返回。

Reactive Stream

对于响应式流的库,有针对 RxJavaJava 9 API响应式流库 的集成:

// 将给定的响应式 Publisher 转换为 Flow
fun Publisher.asFlow(): Flow

这些函数将响应式流转换为了 Flow。

Android 专用 API

对于 Jetpack 库或 Android 平台 API,您可以参阅 Jetpack KTX 库 列表。现有超过 20 个库拥有 KTX 版本,构成了您所熟悉的 Java API。其中包括 SharedPreferences、ViewModels、SQLite 以及 Play Core。

回调

回调是实现异步通讯时非常常见的做法。事实上,我们在 后台线程任务运行指南 中将回调作为 Java 编程语言的默认解决方案。然而,回调也有许多缺点: 这一设计会导致令人费解的回调嵌套。同时,由于没有简单的传播方式,错误处理也更加复杂。在 Kotlin 中,您可以简单地使用协程调用回调,但前提是您必须创建您自己的适配器。

创建您自己的适配器

如果没有找到适合您用例的适配器,更直接的做法是自己编写适配器。对于一次性异步调用,可以使用 suspendCancellableCoroutine API;而对于流数据,可以使用 callbackFlow API。

作为练习,下面的示例将会使用来自 Google Play Services 的 Fused Location Provider API 来获取位置数据。此 API 界面十分简单,但是它使用回调来执行异步操作。当逻辑变得复杂时,这些回调容易使代码变得不可读,而我们可以使用协程来摆脱它们。

如果您希望探索其它解决方案,可以通过上面函数所链接的源代码为您带来启发。

一次性异步调用

Fused Location Provider API 提供了 getLastLocation) 方法来获得 最后已知位置。对于协程来说,理想的 API 是一个直接返回确切结果的挂起函数。

注意: 这一 API 返回值为 Task,并且已经有了对应的 适配器。出于学习的目的,我们用它作为范例。

我们可以通过为 FusedLocationProviderClient 创建扩展函数来获得更好的 API:

suspend fun FusedLocationProviderClient.awaitLastLocation(): Location

由于这是一个一次性异步操作,我们使用 suspendCancellableCoroutine 函数: 一个用于从协程库创建挂起函数的底层构建块。

suspendCancellableCoroutine 会执行作为参数传入的代码块,然后在等待继续信号期间挂起协程的执行。当协程 Continuation 对象中的 resumeresumeWithException 方法被调用时,协程会被恢复执行。有关 Continuation 的更多信息,请参阅: Kotlin Vocabulary | 揭秘协程中的 suspend 修饰符

我们使用可以添加到 getLastLocation 方法中的回调来在合适的时机恢复协程。参见下面的实现:

// FusedLocationProviderClient 的扩展函数,返回最后已知位置
suspend fun FusedLocationProviderClient.awaitLastLocation(): Location =

// 创建新的可取消协程
suspendCancellableCoroutine { continuation ->

// 添加恢复协程执行的监听器
lastLocation.addOnSuccessListener { location ->
// 恢复协程并返回位置
continuation.resume(location)
}.addOnFailureListener { e ->
// 通过抛出异常来恢复协程
continuation.resumeWithException(e)
}

// suspendCancellableCoroutine 块的结尾。这里会挂起协程
//直到某个回调调用了 continuation 参数
}

注意: 尽管协程库中同样包含了不可取消版本的协程构建器 (即 suspendCoroutine),但最好始终选择使用 suspendCancellableCoroutine 处理协程作用域的取消及从底层 API 传播取消事件。

suspendCancellableCoroutine 原理

在内部,suspendCancellableCoroutine 使用 suspendCoroutineUninterceptedOrReturn 在挂起函数的协程中获得 Continuation。这一 Continuation 对象会被一个 CancellableContinuation 对象拦截,后者会从此时开始控制协程的生命周期 (其 实现 具有 Job 的功能,但是有一些限制)。

接下来,传递给 suspendCancellableCoroutine 的 lambda 表达式会被执行。如果该 lambda 返回了结果,则协程将立即恢复;否则协程将会在 CancellableContinuation 被 lambda 手动恢复前保持挂起状态。

您可以通过我在下面代码片段 (原版实现) 中的注释来了解发生了什么:

public suspend inline fun suspendCancellableCoroutine(
crossinline block: (CancellableContinuation) -> Unit
): T =
// 获取运行此挂起函数的协程的 Continuation 对象
suspendCoroutineUninterceptedOrReturn { uCont ->

// 接管协程。Continuation 已经被拦截,
// 接下来将会遵循 CancellableContinuationImpl 的生命周期
val cancellable = CancellableContinuationImpl(uCont.intercepted(), …)
/* … */

// 使用可取消 Continuation 调用代码块
block(cancellable)

// 挂起协程并且等待 Continuation 在 “block” 中被恢复,或者在 “block” 结束执行时返回结果
cancellable.getResult()
}

想了解更多有关挂起函数的工作原理,请参阅这篇: Kotlin Vocabulary | 揭秘协程中的 suspend 修饰符

流数据

如果我们转而希望用户的设备在真实的环境中移动时,周期性地接收位置更新 (使用 requestLocationUpdates) 函数),我们就需要使用 Flow 来创建数据流。理想的 API 看起来应该像下面这样:

fun FusedLocationProviderClient.locationFlow(): Flow

为了将基于回调的 API 转换为 Flow,可以使用 callbackFlow 流构建器来创建新的 flow。callbackFlow 的 lambda 表达式的内部处于一个协程的上下文中,这意味着它可以调用挂起函数。不同于 flow 流构建器,channelFlow 可以在不同的 CoroutineContext 或协程之外使用 offer 方法发送数据。

通常情况下,使用 callbackFlow 构建流适配器遵循以下三个步骤:

  1. 创建使用 offer 向 flow 添加元素的回调;
  2. 注册回调;
  3. 等待消费者取消协程,并注销回调。

将上述步骤应用于当前用例,我们得到以下实现:

// 发送位置更新给消费者
fun FusedLocationProviderClient.locationFlow() = callbackFlow {
// 创建了新的 Flow。这段代码会在协程中执行。
// 1. 创建回调并向 flow 中添加元素
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return // 忽略为空的结果
for (location in result.locations) {
try {
offer(location) // 将位置发送到 flow
} catch (t: Throwable) {
// 位置无法发送到 flow
}
}
}
}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
,这些资料都将为你打开新的学习之门**

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值