如何使用BLoC实现in_app_purchase内购并解决emit调用报错问题
问题原因
- 你遇到的
emit was called after an event handler completed normally报错,核心原因是_onStarted事件处理函数为同步函数,执行完成后BLoC会自动关闭本次事件的emit作用域,而你在purchaseStream的异步监听回调中调用emit时,该作用域已经失效,因此触发报错。 - 次要逻辑问题:代码中多次使用
forEach嵌套async回调,这类异步操作不会被外层逻辑等待,会导致执行顺序混乱,例如completePurchase异步执行未完成就提前emit、商品初始化未完成用户就触发购买等异常。
修复方案
- 使用
emit.forEach管理购买流的监听,BLoC会自动持有emit的有效生命周期,直到流结束或Bloc销毁 - 替换所有
forEach+async的写法为await for,保证异步操作按顺序执行 - 异步初始化方法添加await逻辑,避免时序问题
- 所有emit前可增加
emit.isClosed判断,双重保障不会在作用域失效后调用emit
修复后完整代码
class PaymentBloc extends Bloc<PaymentEvent, PaymentState> { static const bool _started = false; final InAppPurchase _inAppPurchase = InAppPurchase.instance; final Set<String> _kProductIds = {"mini_burger_4_99"}; List<ProductDetails> _products = []; StreamSubscription<List<PurchaseDetails>>? _subscription; PaymentBloc() : super(PaymentInitial(_started)) { on<PaymentStarted>(_onStarted); on<PaymentCompletion>(_buyProduct); } Future<void> _onStarted(PaymentStarted event, Emitter<PaymentState> emit) async { await initStoreInfo(); final purchaseUpdated = _inAppPurchase.purchaseStream; // 使用emit.forEach管理流生命周期,自动持有emit权限 await emit.forEach<List<PurchaseDetails>>( purchaseUpdated, onData: (purchaseDetailsList) async* { await for (final purchaseDetails in purchaseDetailsList) { if (purchaseDetails.status == PurchaseStatus.pending) { yield PaymentPending(); } else { if (purchaseDetails.status == PurchaseStatus.error) { yield PaymentError(); } else if (purchaseDetails.pendingCompletePurchase) { await _inAppPurchase.completePurchase(purchaseDetails); yield PaymentComplete(); } } } }, onError: (error, stackTrace) { return PaymentError(); }, ); } Future<void> _buyProduct( PaymentCompletion event, Emitter<PaymentState> emit) async { final paymentWrapper = SKPaymentQueueWrapper(); final transactions = await paymentWrapper.transactions(); // 替换forEach+async为await for保证异步操作顺序 await for (final transaction in transactions) { await paymentWrapper.finishTransaction(transaction); } try { final purchaseParam = PurchaseParam( productDetails: _products.firstWhere((product) => product.id == event.id)); await _inAppPurchase.buyConsumable(purchaseParam: purchaseParam); } catch (e) { if (!emit.isClosed) { emit(PaymentError()); } } } @override Future<void> close() { _subscription?.cancel(); return super.close(); } Future<void> initStoreInfo() async { final bool isAvailable = await _inAppPurchase.isAvailable(); if (!isAvailable) { _products = []; return; } if (Platform.isIOS) { final iosPlatformAddition = _inAppPurchase .getPlatformAddition<InAppPurchaseIosPlatformAddition>(); await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); } final productDetailResponse = await _inAppPurchase.queryProductDetails(_kProductIds); _products = productDetailResponse.productDetails; } }
状态优化建议
如果你只需要布尔值控制UI,可将状态类合并为单类,简化调用:
class PaymentState { final bool isPending; final bool isSuccess; final bool isError; PaymentState({ this.isPending = false, this.isSuccess = false, this.isError = false, }); PaymentState copyWith({ bool? isPending, bool? isSuccess, bool? isError, }) { return PaymentState( isPending: isPending ?? this.isPending, isSuccess: isSuccess ?? this.isSuccess, isError: isError ?? this.isError, ); } } // emit时直接更新对应字段即可:emit(state.copyWith(isPending: true))
内容的提问来源于stack exchange,提问作者Matt199
相关产品推荐
相关产品推荐

