You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 09:50:24