Flutter Clean Architecture下用BLoC处理多Stream UseCase的疑问
处理多Stream UseCase的BLoC可行方案
方案一:单BLoC合并多Stream到统一State
不用拆分多个BLoC,你可以创建一个包含所有数据类型的不可变State类,把两个Stream的数据、加载状态、错误信息都纳入其中。在BLoC内部同时监听这两个Stream,任一Stream有新数据时,更新State对应字段并输出新State。
示例代码
定义统一State
class CombinedDataState extends Equatable { final List<TypeA> dataA; final List<TypeB> dataB; final bool isLoadingA; final bool isLoadingB; final String? errorA; final String? errorB; const CombinedDataState({ this.dataA = const [], this.dataB = const [], this.isLoadingA = false, this.isLoadingB = false, this.errorA, this.errorB, }); CombinedDataState copyWith({ List<TypeA>? dataA, List<TypeB>? dataB, bool? isLoadingA, bool? isLoadingB, String? errorA, String? errorB, }) { return CombinedDataState( dataA: dataA ?? this.dataA, dataB: dataB ?? this.dataB, isLoadingA: isLoadingA ?? this.isLoadingA, isLoadingB: isLoadingB ?? this.isLoadingB, errorA: errorA ?? this.errorA, errorB: errorB ?? this.errorB, ); } @override List<Object?> get props => [dataA, dataB, isLoadingA, isLoadingB, errorA, errorB]; }
BLoC内部处理多Stream
class CombinedDataBloc extends Bloc<CombinedDataEvent, CombinedDataState> { final FetchTwoStreamsUseCase _useCase; StreamSubscription? _dataASub; StreamSubscription? _dataBSub; CombinedDataBloc(this._useCase) : super(const CombinedDataState()) { on<LoadDataEvent>((event, emit) async { emit(state.copyWith(isLoadingA: true, isLoadingB: true)); // 监听第一个Stream _dataASub?.cancel(); _dataASub = _useCase.streamA.listen( (data) => emit(state.copyWith(dataA: data, isLoadingA: false, errorA: null)), onError: (err) => emit(state.copyWith(errorA: err.toString(), isLoadingA: false)), ); // 监听第二个Stream _dataBSub?.cancel(); _dataBSub = _useCase.streamB.listen( (data) => emit(state.copyWith(dataB: data, isLoadingB: false, errorB: null)), onError: (err) => emit(state.copyWith(errorB: err.toString(), isLoadingB: false)), ); }); } @override Future<void> close() { _dataASub?.cancel(); _dataBSub?.cancel(); return super.close(); } }
页面层使用
BlocBuilder<CombinedDataBloc, CombinedDataState>( builder: (context, state) { return Column( children: [ if (state.isLoadingA) const CircularProgressIndicator(), if (state.errorA != null) Text('A数据加载失败:${state.errorA}'), Expanded(child: DataAList(data: state.dataA)), const SizedBox(height: 16), if (state.isLoadingB) const CircularProgressIndicator(), if (state.errorB != null) Text('B数据加载失败:${state.errorB}'), Expanded(child: DataBList(data: state.dataB)), ], ); }, )
这个方案的优势是:保持单个BLoC对应页面逻辑,聚合性强,无需处理多个BLoC的状态同步问题。
方案二:同一页面使用多个独立BLoC
如果两类数据逻辑完全独立,未来可能单独复用,也可以在页面中嵌套多个BlocProvider,每个BLoC对应一个数据Stream。
示例代码
页面注入多BLoC
MultiBlocProvider( providers: [ BlocProvider(create: (context) => DataABloc(fetchDataAUseCase)), BlocProvider(create: (context) => DataBBloc(fetchDataBUseCase)), ], child: YourTargetPage(), )
页面分别渲染
Column( children: [ BlocBuilder<DataABloc, DataAState>( builder: (context, state) { if (state is DataALoading) return const CircularProgressIndicator(); if (state is DataAError) return Text('错误:${state.message}'); if (state is DataALoaded) return DataAList(data: state.data); return const SizedBox(); }, ), BlocBuilder<DataBBloc, DataBState>( builder: (context, state) { if (state is DataBLoading) return const CircularProgressIndicator(); if (state is DataBError) return Text('错误:${state.message}'); if (state is DataBLoaded) return DataBList(data: state.data); return const SizedBox(); }, ), ], )
这个方案的优势是:每个BLoC职责单一,逻辑清晰,便于单独测试和维护。
方案三:Rx操作符合并流(进阶)
如果需要两类数据联动(比如必须同时获取到两类数据才渲染完整页面),可以用rxdart的CombineLatest等操作符合并两个Stream,再在BLoC中处理合并后的流。
示例代码
class CombinedDataBloc extends Bloc<CombinedDataEvent, CombinedDataState> { final FetchTwoStreamsUseCase _useCase; StreamSubscription? _combinedSub; CombinedDataBloc(this._useCase) : super(const CombinedDataState()) { on<LoadDataEvent>((event, emit) async { emit(state.copyWith(isLoadingA: true, isLoadingB: true)); final combinedStream = CombineLatestStream.combine2( _useCase.streamA, _useCase.streamB, (List<TypeA> a, List<TypeB> b) => (dataA: a, dataB: b), ); _combinedSub?.cancel(); _combinedSub = combinedStream.listen( (pair) => emit(state.copyWith( dataA: pair.dataA, dataB: pair.dataB, isLoadingA: false, isLoadingB: false, errorA: null, errorB: null, )), onError: (err) => emit(state.copyWith( errorA: err is DataAException ? err.toString() : null, errorB: err is DataBException ? err.toString() : null, isLoadingA: false, isLoadingB: false, )), ); }); } @override Future<void> close() { _combinedSub?.cancel(); return super.close(); } }
总结
- 若两类数据是页面核心关联数据,优先选单BLoC合并State方案,逻辑聚合性强
- 若数据逻辑完全独立、可能复用,选多BLoC拆分方案,职责单一
- 若需数据联动渲染,考虑Rx操作符合并流的进阶方案
内容的提问来源于stack exchange,提问作者Mackovich
相关产品推荐
相关产品推荐

