Search code examples
flutterdartbloc

Flutter bloc how to orchestra among blocs in more clean way


I have several blocs to check some app status, like connectivity, battery, etc.

And I would like to have a integrated bloc to keep track of each blocs status, ie. a summary bloc that will orchestra the status returned from each status bloc.

For example, the after sequential of running of each status bloc, connectivity checking, battery checking, ..., the summary bloc will collect the result of each bloc and finally emit a state that showing the final result of each bloc.

What is the best way to communicate among them?

Currently I can think of "Connecting Blocs through Presentation", by using BlocListener to each bloc state change, I call subsequent blocs to perform its own task, and waiting the final bloc state change to tell the summary bloc to do the summary.

But I think it is too much in a presentation layer to just orchestra the blocs communication.

The other way can be move down to the layer (as also suggested in the official doc), but I cannot come up a way to move it down to a repository.

So what is the best way to deal with this situation?

Thanks

What I have tried in code example:

MultiBlocProvider(
      providers: [
        BlocProvider(
          create: (context) => ConnectivityCheckCubit(),
        ),
        BlocProvider(
          create: (context) => BatteryCheckCubit(),
        ),
        BlocProvider(
          create: (context) => SummaryCubit(),
        ),
      ],


MultiBlocListener(
          listeners: [
            BlocListener<ConnectivityCheckCubit, ConnectivityCheckState>(
              listener: (context, state) {
                if (state is ConnectivityCheckFailure) {
                  context.read<SummaryCubit>().connectivityFailed();
                } else if (state is ConnectivityCheckSuccess) {
                  context.read<SummaryCubit>().connectivitySucceed();
                }
              },
            ),
            BlocListener<BatteryCheckCubit, BatteryCheckState>(
              listener: (context, state) {
                if (state is BatteryCheckSuccess) {
                  context.read<SummaryCubit>().battery(state.battery);
                } 
              },
            ),
            BlocListener<SummaryCubit, SummaryState>(
              listener: (context, state) {
                if (state is SummaryCheckConnectivity) {
                  if (state.status == SummaryStatus.failure) {
                    //show connectivity warning
                  } else if (state.status ==
                      SummaryStatus.success) {
                    context.read<BatteryCheckCubit>().checkBattery();
                  }
                } else if (state is SummaryCheckBattery) {
                  if (state.status == SummaryStatus.failure) {
                    //show battery warning
                  } 
                } else if (state is SummaryCheckSuccess) {
                  //show a summary report to user
                }
              },
            ),
          ],

Solution

  • right direction. since this is a block and streams under the hood, you have two options for block communication: either the presentation layer(that is OK with bloc's) or at the repository level (the third option is to listen to block streams of ConnectivityCheckCubit and BatteryCheckCubit in the SummaryCubit, although this is a completely viable option it is not recommended in the official documentation). in the first case, you could make a separate widget:

    class CheckWidget extends StatelessWidget {
      const CheckWidget({
        required this.child,
        super.key,
      });
    
      final Widget child;
    
      @override
      Widget build(BuildContext context) {
        return MultiBlocListener(
          listeners: [
            BlocListener<ConnectivityCheckCubit, ConnectivityCheckState>(
              listener: (context, state) {
                if (state is ConnectivityCheckFailure) {
                  context.read<SummaryCubit>().connectivityFailed();
                } else if (state is ConnectivityCheckSuccess) {
                  context.read<SummaryCubit>().connectivitySucceed();
                }
              },
            ),
            BlocListener<BatteryCheckCubit, BatteryCheckState>(
              listener: (context, state) {
                if (state is BatteryCheckSuccess) {
                  context.read<SummaryCubit>().battery(state.battery);
                }
              },
            ),
          ],
          child: child,
        );
      }
    }
    

    then where you need it you will use listener or consumer:

    class Page extends StatelessWidget {
      const Page({
        super.key,
      });
    
      @override
      Widget build(BuildContext context) {
        return CheckWidget(
          child: BlocListener<SummaryCubit, SummaryState>(
            listener: (context, state) {
              if (state is SummaryCheckConnectivity) {
                if (state.status == SummaryStatus.failure) {
                  //show connectivity warning
                } else if (state.status == SummaryStatus.success) {
                  context.read<BatteryCheckCubit>().checkBattery();
                }
              } else if (state is SummaryCheckBattery) {
                if (state.status == SummaryStatus.failure) {
                  //show battery warning
                }
              } else if (state is SummaryCheckSuccess) {
                //show a summary report to user
              }
            },
            child: Container(),
          ),
        );
      }
    }
    

    for the second option with repositories the situation is changing somewhat. you probably shouldn't use ConnectivityCheckCubit and BatteryCheckCubit. I assume that when checking the battery and connection, it is checked on the platform level. even for the first case, this should happen not in the blocks themselves, but in separate repositories e.g. ConnectivityCheckRepo and BatteryCheckRepothat that send data to these blocks correspondingly.

    thus, in the second case, these repositories will send the result of the check not to blocks but to the repository SummaryRepo or common. which in turn will already interact with the SummaryCubit. The pseudocode for the second case will be something like this:

    import 'dart:async';
    
    import 'package:connectivity_plus/connectivity_plus.dart';
    import 'package:flutter_bloc/flutter_bloc.dart';
    import 'package:internet_connection_checker/internet_connection_checker.dart';
    
    
    class ConnectivityCheckRepo extends Cubit<NetworkStateType?> {
      ConnectivityCheckRepo() : super(null) {
        _startListener();
      }
    
      static const _tag = 'ConnectivityCheckRepo';
    
      final _connectivity = Connectivity();
      late final StreamSubscription? _subscription;
    
      Future<void> _startListener() async {
        await _getPhoneInfo(await _connectivity.checkConnectivity());
        _subscription = _connectivity.onConnectivityChanged.listen(_getPhoneInfo);
      }
    
      @override
      Future<void> close() {
        _subscription?.cancel();
        return super.close();
      }
    
      Future<void> _getPhoneInfo(ConnectivityResult result) async {
        try {
          final hasConnection = await Future.delayed(
            const Duration(milliseconds: 200),
                () async => InternetConnectionChecker().hasConnection,
          );
          final stateType =
          hasConnection ? NetworkStateType.good : NetworkStateType.none;
          print('[$_tag], $stateType');
          emit(stateType);
        } catch (error, stackTrace) {
          print('[$_tag], $error, $stackTrace');
        }
      }
    }
    
    enum NetworkStateType {
      none,
      bad,
      good,
    }
    

    main repo logic could be extracted to the base repo. you could experimenting with logic here e.g.:

    abstract class BaseRepository<T> {
      BaseRepository({
        required ConnectivityCheckRepo connectivityCheckRepository,
        //required BatteryCheckRepo batteryCheckRepoRepository,
      }) {
        _currentConnectionType = connectivityCheckRepository.state;
        _connectivitySubscription = connectivityCheckRepository.stream.listen(
              (type) async {
            print(type);
            _currentConnectionType = type;
    
            switch (type) {
              case NetworkStateType.none:
                await onGoingOffline();
                break;
              case NetworkStateType.bad:
              case NetworkStateType.good:
              case null:
                await onGoingOnline();
                break;
            }
          },
        );
        //...
        //similar code for batteryCheckRepoRepository
      }
    
      late final StreamSubscription _connectivitySubscription;
      NetworkStateType? _currentConnectionType;
    
      final _controller = StreamController<T>();
    
      Stream<T> getCheckingEvent() => _controller.stream;
    
      void addToStream(T event) => _controller.sink.add(getCheckingEvent() as T);
    
      String get repoTag;
    
      Future<void> onGoingOnline();
    
      Future<void> onGoingOffline();
    
      Future<void> dispose() async {
        await _connectivitySubscription.cancel();
        await _controller.close();
      }
    
      //add methods for battery checking
    }
    

    then in SummaryRepo:

    class SummaryRepo extends BaseRepository<SomeEventForCubitFromRepo> {
      SummaryRepo({required super.connectivityCheckRepository});
    
      @override
      String get repoTag => 'SummaryRepo';
    
      @override
      Future<void> onGoingOffline() async {
        addToStream(SomeEventForCubitFromRepo('onGoingOffline'));
      }
    
      @override
      Future<void> onGoingOnline() async {
        addToStream(SomeEventForCubitFromRepo('onGoingOnline'));
      }
    }
    

    where SomeEventForCubitFromRepo is some data that you need in SummaryCubit on every event. Ideally, these should be Equatable or freezed models:

    class SomeEventForCubitFromRepo {
      SomeEventForCubitFromRepo(this.someData);
    
      final String someData;
    
    ...add your data
    
    }
    

    and finally you could listen this events from repo in your SummaryCubit:

    class SummaryCubit extends Cubit<SomeEventForCubitFromRepo> {
      SummaryCubit({required this.summaryRepo}) : super(SomeEventForCubitFromRepo('initial')) {
        _startListener();
      }
    
      final SummaryRepo summaryRepo;
      late final StreamSubscription _subscription;
    
      Future<void> _startListener() async {
        _subscription = summaryRepo.getCheckingEvent().listen((event) {
          emit(event);
        });
      }
    
      @override
      Future<void> close() {
        _subscription.cancel();
        return super.close();
      }
    }