如何优雅解决Flutter BLoC中Event触发API请求的竞态问题
解决方式核心思路
这类连续请求的竞态问题本质是旧的异步任务完成后依然会执行后续的emit逻辑,覆盖了新状态,只需要让新事件触发时自动取消前一个未完成的同类型异步任务,或者执行emit前校验当前任务是否是最新的即可。
方案1:使用官方提供的并发转换器(最优雅,可全局生效)
Bloc官方配套的bloc_concurrency包提供了现成的事件转换器,其中restartable转换器的作用就是:每当同类型的新事件进来时,自动取消前一个还在处理中的该类事件的异步任务,刚好匹配你的需求。
单个Bloc适配代码
修改你示例里的事件注册逻辑即可:
import 'package:bloc_concurrency/bloc_concurrency.dart'; class ExampleBloc extends Bloc<ExampleEvent, ExampleState> { ExampleBloc() : super(ExampleDataLoadInProgress(DateTime.now())) { // 只需要加一行transformer参数 on<ExampleSelectedDateChanged>( (event, emit) async { await _fetchData(event.date, emit); }, transformer: restartable(), ); } // 你的_fetchData方法不需要做任何修改 Future<void> _fetchData( DateTime selectedDate, Emitter<ExampleState> emit, ) async { emit(ExampleDataLoadInProgress(selectedDate)); try { final data = await callAPI(selectedDate); emit(ExampleDataLoadSuccess(data, selectedDate)); } on ApiException catch (e) { emit(ExampleDataLoadFailure(e, selectedDate)); } } }
全局生效配置
如果要让应用内所有Bloc的事件默认都走restartable逻辑,不需要每个事件都加transformer参数,可以通过自定义基础Bloc类实现:
import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; abstract class BaseBloc<Event, State> extends Bloc<Event, State> { BaseBloc(State initialState) : super(initialState); // 重写on方法,默认使用restartable转换器 @override void on<Event extends Object>( EventHandler<Event, State> handler, { EventTransformer<Event>? transformer, }) { super.on( handler, // 没有传自定义transformer时默认用restartable transformer: transformer ?? restartable(), ); } } // 后续所有业务Bloc都继承BaseBloc即可,不需要额外加参数 class ExampleBloc extends BaseBloc<ExampleEvent, ExampleState> { ExampleBloc() : super(ExampleDataLoadInProgress(DateTime.now())) { // 直接注册事件即可,默认自动取消旧任务 on<ExampleSelectedDateChanged>((event, emit) async { await _fetchData(event.date, emit); }); } }
如果部分事件不需要自动取消旧任务,只要在注册时手动传其他transformer(比如sequential()串行执行、droppable()丢弃新事件)即可,灵活性很高。
方案2:无依赖手动实现(适合不想引入额外包的场景)
如果不想新增依赖,可以通过标记最新请求标识的方式,在emit前校验当前请求是否有效:
class ExampleBloc extends Bloc<ExampleEvent, ExampleState> { // 存储当前最新的请求对应的日期,或者用自增ID做标识也可以 DateTime? _latestSelectedDate; ExampleBloc() : super(ExampleDataLoadInProgress(DateTime.now())) { on<ExampleSelectedDateChanged>((event, emit) async { // 每次新事件进来更新最新标识 _latestSelectedDate = event.date; await _fetchData(event.date, emit); }); } Future<void> _fetchData( DateTime selectedDate, Emitter<ExampleState> emit, ) async { emit(ExampleDataLoadInProgress(selectedDate)); try { final data = await callAPI(selectedDate); // emit前校验当前请求的日期是不是最新的,不是就直接丢弃 if (selectedDate == _latestSelectedDate) { emit(ExampleDataLoadSuccess(data, selectedDate)); } } on ApiException catch (e) { if (selectedDate == _latestSelectedDate) { emit(ExampleDataLoadFailure(e, selectedDate)); } } } }
这个方案也可以封装到BaseBloc里实现全局复用,只需要把最新标识的逻辑放到基类即可。
内容的提问来源于stack exchange,提问作者RMK
相关产品推荐
相关产品推荐

