如何将Stripe自定义回调转为Kotlin协程解决Flow emit报错
错误原因
flow构建器本身对执行上下文有严格约束:emit方法仅允许在构建器自身的协程上下文内顺序调用,不允许在其他线程/异步回调上下文中跨上下文调用。
你代码里的PaymentIntentCallback是Stripe SDK在内部工作线程触发的,和Flow的执行上下文完全隔离,直接在回调里调用emit既不符合Flow的上下文规则,也存在线程安全问题,IDE才会提示错误,强行运行也会抛出上下文不一致的异常。
修复实现
对于这种单次返回结果的回调式API,标准协程实现分两步:先通过suspendCancellableCoroutine把回调API转成挂起函数,再在Flow中顺序调用、发射结果,全程不会有上下文冲突。
完整可运行代码:
// 1. 先把Stripe回调包装为支持取消的挂起函数,可单独复用 private suspend fun fetchPaymentIntent(clientSecret: String): PaymentIntent = suspendCancellableCoroutine { continuation -> Terminal.getInstance().retrievePaymentIntent(clientSecret, object : PaymentIntentCallback { override fun onFailure(e: TerminalException) { // 协程仍活跃时才恢复,避免取消后抛异常 if (continuation.isActive) { continuation.resumeWith(Result.failure(e)) } } override fun onSuccess(paymentIntent: PaymentIntent) { if (continuation.isActive) { continuation.resumeWith(Result.success(paymentIntent)) } } }) // 协程被取消时同步清理Stripe侧请求,若SDK无取消方法可省略 continuation.invokeOnCancellation { // 例如: Terminal.getInstance().cancelCurrentRequest() } } // 2. 实现Flow接口,所有emit操作都在Flow上下文内顺序执行 override fun retrievePaymentIntent(clientSecret: String): Flow<Resource<PaymentIntent>> = flow { emit(Resource.Loading()) runCatching { fetchPaymentIntent(clientSecret) }.onSuccess { intent -> emit(Resource.Success(intent)) }.onFailure { e -> emit(Resource.Error(e as? TerminalException, e.localizedMessage ?: "获取支付信息失败")) } }
其他可选方案
如果你希望直接用Flow包装回调,不需要单独抽挂起函数,可以使用专门为跨上下文发送事件设计的callbackFlow构建器,它提供的trySend方法是线程安全的,支持在任意回调线程发送值:
override fun retrievePaymentIntent(clientSecret: String): Flow<Resource<PaymentIntent>> = callbackFlow { // 先发送加载态 trySend(Resource.Loading()) val callback = object : PaymentIntentCallback { override fun onFailure(e: TerminalException) { trySend(Resource.Error(e, e.localizedMessage ?: "获取支付信息失败")) close() // 单次请求结束后关闭流 } override fun onSuccess(paymentIntent: PaymentIntent) { trySend(Resource.Success(paymentIntent)) close() // 单次请求结束后关闭流 } } Terminal.getInstance().retrievePaymentIntent(clientSecret, callback) // 流被收集者取消时触发清理 awaitClose { // 可选:取消Stripe侧正在执行的请求 } }
注意:使用
callbackFlow时,在单次结果返回后必须调用close()关闭流,否则流会一直保持活跃状态,造成不必要的资源占用。
注意事项
- 不要用
GlobalScope或者其他外部协程作用域在回调里切协程再调用emit,会破坏Flow的结构化并发规则,引发内存泄漏、异常丢失、协程泄漏等问题。 - 单次返回结果的回调优先用
suspendCancellableCoroutine实现,callbackFlow更适合需要持续推送多个值的回调场景(例如实时位置监听、蓝牙设备扫描回调、长连接消息推送等)。 - 恢复协程前必须判断
isActive状态,避免协程已经被取消后调用resume抛出异常。
内容的提问来源于stack exchange,提问作者matghazaryan
相关产品推荐
相关产品推荐

