把回调API包装成协程API,并不只是把回调内容搬进一个构建器。真正需要转换的是异步协议:一次调用究竟产生一个最终结果、持续产生多个事件,还是由多个并发生产者共同生成数据。
协议判断错误时,代码即使能够运行,也会隐藏结果丢失、重复恢复、监听器泄漏和顺序冲突。稳定的桥接方案应同时定义结果数量、失败通道、取消传播、资源释放以及多来源结果的优先级。
内容摘要
- 单次最终结果使用
suspendCancellableCoroutine,成功和失败只能完成一次。 - 持续回调使用
callbackFlow,通过trySend发送数据,并检查发送结果。 awaitClose负责在收集取消或Flow关闭时注销监听器、释放资源。- 多个协程并发生产数据使用
channelFlow,但结果顺序和覆盖规则仍由业务定义。 - 桥接层必须让协程取消尽量传递到底层请求,形成完整生命周期闭环。

一、桥接之前先确认结果协议
外部API常见的三种结果模型是:
一次请求返回一个最终结果
持续监听并产生多个事件
多个异步来源并发产生结果
它们分别对应不同抽象:
单次结果 → suspendCancellableCoroutine
持续事件 → callbackFlow + awaitClose
多协程并发生产 → channelFlow
这里的关键不是回调函数数量,而是一次订阅期间允许产生多少个有效结果。一个接口即使同时提供onSuccess和onError,只要二者互斥地表达一次最终完成,仍然属于单次结果。
反过来,状态从"已提交"变化到"传输中",最后再进入"成功"或"失败",已经是一个时间序列。强行压成一次返回值会丢失中间状态,也无法表达后续更新。
本节小结
回调桥接首先是协议建模。结果数量决定使用挂起函数还是Flow,生产者数量再决定是否需要channelFlow。
二、单次结果如何安全转换为挂起函数
kotlin
suspend fun awaitLocation(): Location =
suspendCancellableCoroutine { continuation ->
// 把外部单次回调映射成一次挂起函数调用
val callback = object : LocationCallback {
override fun onSuccess(value: Location) {
// 成功回调恢复协程,并把 value 作为函数返回值
continuation.resume(value)
}
override fun onError(error: Throwable) {
// 失败回调恢复协程,并让调用方收到异常
continuation.resumeWithException(error)
}
}
// 注册回调后,当前协程保持挂起等待最终结果
locationClient.request(callback)
continuation.invokeOnCancellation {
// 调用方取消时,尽量同步取消底层请求并解除引用
locationClient.cancel(callback)
}
}
这段代码建立了三条转换路径:
scss
回调成功 → resume(value) → 挂起函数返回
回调失败 → resumeWithException(error) → 挂起函数抛出异常
协程取消 → invokeOnCancellation → 取消请求或注销回调
Continuation只能完成一次
挂起函数一次调用只能返回一个值或抛出一个异常。底层SDK如果重复触发最终回调,第二次调用resume就会出错。
如果SDK可能重复回调,需要确保只有第一次最终结果生效:
kotlin
// 多个回调同时到达时,AtomicBoolean 只允许其中一个把状态改为 true
val completed = AtomicBoolean(false)
fun complete(block: () -> Unit) {
if (completed.compareAndSet(false, true)) {
// 只有第一次从 false 更新为 true 的调用能够执行完成逻辑
block()
}
}
第一条最终结果完成Continuation,后续重复结果忽略并记录。如果同一请求先成功后失败,这不是普通重复,而是协议冲突;应结合请求标识、状态机和日志定位来源,不能用"最后一次结果覆盖前一次"处理。
取消后的竞态仍需考虑
取消和回调可能几乎同时发生。suspendCancellableCoroutine提供可取消Continuation,但底层资源不会因此自动停止。invokeOnCancellation必须尽量调用SDK的取消请求或反注册接口。
清理逻辑还应具备幂等性,因为正常完成、主动取消和SDK自身结束都可能触发相邻的资源释放路径。
工程场景:厂商SDK返回重复或相互矛盾的结果
部分设备SDK可能在网络切换、内部重试或跨进程恢复后产生异常回调序列:
同一requestId连续回调两次成功
先回调成功,随后又回调失败
页面退出并取消协程后,旧回调仍然到达
新请求启动后,旧请求结果错误写入新页面
只检查isActive并不能防止重复恢复。检查结束后,协程可能立即被取消;两个回调也可能几乎同时到达。比较稳妥的做法,是另外记录这次请求是否已经结束:只有第一个结果可以恢复协程,后续结果只记录日志。
kotlin
private enum class TerminalState {
// 显式记录第一次被接受的最终状态,便于识别冲突回调
Success,
Failure
}
suspend fun awaitDeviceResult(
requestId: String
): DeviceResult = suspendCancellableCoroutine { continuation ->
// null 表示请求还没有收到最终结果
val terminal = AtomicReference<TerminalState?>(null)
val callback = object : DeviceCallback {
override fun onSuccess(
callbackRequestId: String,
value: DeviceResult
) {
// 丢弃其他请求的迟到回调,防止旧结果污染当前调用
if (callbackRequestId != requestId) {
logger.warn("ignore mismatched request")
return
}
// 只有第一个回调能够把请求标记为成功
val accepted = terminal.compareAndSet(
null,
TerminalState.Success
)
if (!accepted) {
logger.error(
"conflicting terminal callback: " +
"${terminal.get()} -> success"
)
return
}
// 正常完成路径也要注销监听器,不能只在取消时清理
deviceSdk.unregister(this)
if (continuation.isCancelled) {
// 协程已经取消,迟到结果只记录,不再恢复业务代码
logger.warn("late success after cancellation")
return
}
// 前面的 CAS 保证这里最多执行一次
continuation.resume(value)
}
override fun onError(
callbackRequestId: String,
error: Throwable
) {
// 错误结果也必须先验证所属请求
if (callbackRequestId != requestId) return
// 成功和失败都在争取第一次完成这次请求
val accepted = terminal.compareAndSet(
null,
TerminalState.Failure
)
if (!accepted) {
logger.error(
"conflicting terminal callback: " +
"${terminal.get()} -> failure"
)
return
}
// 请求结束后立即解除 SDK 对 callback 的持有
deviceSdk.unregister(this)
if (continuation.isCancelled) {
logger.warn("late failure after cancellation")
return
}
// 让挂起函数以异常结束,交由调用方处理失败
continuation.resumeWithException(error)
}
}
// requestId 同时用于发起请求和校验回调归属
deviceSdk.start(requestId, callback)
continuation.invokeOnCancellation {
// 取消请求与注销回调都必须支持并发或重复调用
deviceSdk.cancel(requestId)
deviceSdk.unregister(callback)
}
}
这段适配代码明确处理了四个边界:
requestId隔离不同请求,避免旧结果污染新调用。AtomicReference保证恢复方法最多调用一次,并识别重复或相互矛盾的回调。- CancellableContinuation保证并发的取消与单次恢复只有一方成功。
- 正常完成和取消路径都注销回调,
invokeOnCancellation还会尽量终止底层工作。
注销与取消接口必须允许并发或重复调用,因为回调与取消可能同时发生。日志还可以记录时间、设备型号和SDK版本,帮助判断问题来自业务调用、厂商实现还是进程切换。适配代码需要保护协程调用方,但不能把SDK返回了矛盾结果这件事悄悄隐藏起来。
本节小结
单次回调桥接的完整契约是:成功返回、失败抛出、取消清理、只完成一次。缺少任意一项,都只是语法转换,还没有完成生命周期转换。
三、持续回调如何转换为Flow
kotlin
fun locationUpdates(): Flow<Location> = callbackFlow {
// callbackFlow 适合一次订阅期间持续产生多个回调结果
val listener = object : LocationListener {
override fun onLocation(value: Location) {
// 普通回调不能调用挂起的 send,因此使用非挂起 trySend
trySend(value)
.onFailure { cause ->
// 发送失败表示数据没有进入 Flow,必须显式记录或处理
logger.warn("location delivery failed", cause)
}
}
override fun onError(error: Throwable) {
// 外部数据源失败时,携带异常关闭 Flow
close(error)
}
}
// 开始收集时注册外部监听器
locationClient.register(listener)
awaitClose {
// 收集取消或 Flow 关闭后注销监听器,形成资源闭环
locationClient.unregister(listener)
}
}
callbackFlow内部使用Channel接收来自外部回调的数据,因此回调可以从不同线程安全地提交结果。普通回调不是挂起函数,通常不能直接调用挂起的send(),所以使用非挂起的trySend()。
trySend成功不等于消费完成
trySend(value)成功只表示数据已被Channel接受,不代表下游业务已经处理完成。失败则表示数据没有进入Flow,返回的ChannelResult必须被检查:
vbnet
解析或转换代码抛异常 → try-catch处理
数据未能进入Channel → ChannelResult.onFailure处理
外部数据源终止并报错 → close(error)传递给下游
不能只用try-catch包裹trySend后就认为发送失败已被处理,因为发送失败通常通过结果对象表达。
awaitClose建立资源闭环
注册监听器以后,生产端需要保持有效,直到下游停止收集或Flow主动关闭。awaitClose同时承担等待和最终清理:
arduino
开始收集
→ 注册监听器
→ 多次trySend
→ 收集取消或close
→ awaitClose执行
→ 注销监听器
如果省略注销,页面即使不再收集,SDK仍可能持有监听器并持续回调,最终造成资源浪费或对象泄漏。
本节小结
callbackFlow负责把持续回调转换为数据流,trySend负责非挂起发送,close(error)负责结束异常通道,awaitClose负责生命周期收尾。四者共同构成完整桥接。
四、多个生产者如何汇入同一条Flow
普通flow {}强调同一个生产协程按顺序调用emit。当多个子协程需要并发产生数据时,可以使用channelFlow:
kotlin
fun userSources(): Flow<UserResult> = channelFlow {
// 两个子协程可以并发生产,并安全汇入同一条 Flow
launch {
// 缓存结果可能更快到达,但不代表业务优先级更高
send(UserResult.Cache(database.loadUser()))
}
launch {
// 网络结果完成顺序不确定,下游仍需制定覆盖规则
send(UserResult.Network(api.loadUser()))
}
}
channelFlow保证并发发送的安全性,却不保证缓存和网络谁先完成:
安全汇合 ≠ 业务有序
如果网络数据必须覆盖缓存,结果模型至少需要携带来源、版本或时间戳,并由明确规则决定是否接纳。不能把协程完成顺序直接当作业务优先级。
channelFlow也不应替代所有普通Flow。单一生产者顺序发射时,flow {}的语义更直接;外部持续回调使用callbackFlow更能表达注册与注销关系。
本节小结
channelFlow解决多协程并发发送的安全汇合,业务层仍需解决顺序、去重和覆盖优先级。并发容器不会自动生成正确的数据策略。
结语:桥接的是协议,也是生命周期
从回调到协程的选择模型可以归纳为:
一个最终结果 → suspendCancellableCoroutine
持续多个事件 → callbackFlow + awaitClose
并发多个来源 → channelFlow + 明确合并规则
无论选择哪一种抽象,桥接层都必须完整表达成功、失败、取消和清理。协程API的价值不只在于消除回调嵌套,更在于把外部异步协议纳入可管理、可取消、可验证的生命周期结构。