SingleLiveEvent pattern implementation for Cubit and Bloc

Pub codecov style: lint

Bloc Effects Package

The abstractions on Cubit and Bloc and Flutter Widget that make it easy to add UI Effects to the BLoC state management using [package:bloc](https://pub.dev/packages/bloc).

Usage

Lets take a look at how to use mixin Effects or class CubitWithEffects to dispatch effects from CounterCubit to a CounterPage and react on them with BlocEffectListener.

ui_effect.dart

abstract class UiEffect {}

class ShowBottomSheet implements UiEffect {
 const ShowBottomSheet({required this.counterValue});
 
 final int counterValue;
}

counter_cubit.dart

class CounterCubit extends Cubit<int> with Effects<UiEffect> {
 CounterCubit() : super(0);

 void increment() => emit(state + 1);

 void onButtonPressed() => emitEffect(ShowBottomSheet(counterValue: state));
}

or the same with the class extension:

class CounterCubit extends CubitWithEffects<int, UiEffect> {
 CounterCubit() : super(0);

 void increment() => emit(state + 1);

 void onButtonPressed() => emitEffect(ShowBottomSheet(counterValue: state));
}

main.dart

void main() => runApp(CounterApp());

class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterCubit(),
        child: CounterPage(),
      ),
    );
  }
}

counter_page.dart

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: BlocEffectListener<CounterCubit, UiEffect>(
        listener: (context, effect) {
          if (effect is ShowBottomSheet) {
            showBottomSheet<void>(
              context: context,
              builder: (c) =>
                  Material(
                    child: Container(
                      color: Colors.black12,
                      height: 150,
                    ),
                  ),
            );
          }
        },
        child: const SizedBox(),
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Show effect',
        child: const Icon(Icons.upload),
        onPressed: () => context.read<CounterCubit>().onButtonPressed(),
      ),
    );
  }
}

And also BlocWithEffectsObserver can be useful. It extends BlocObserver and add onEffect callback to base observing features:

app_bloc_observer.dart

class AppBlocObserver extends BlocWithEffectsObserver {
 const AppBlocObserver();

 @override
 void onBlocEffect(BlocBase<dynamic> bloc, Object? effect) {
   super.onBlocEffect(bloc, effect);
   debugPrint('${bloc.runtimeType} emitted $effect');
 }

 // Other BlocObserver overrides.
}

Install the observer before creating any blocs or cubits:

void main() {
  Bloc.observer = const AppBlocObserver();
  runApp(const CounterApp());
}

BlocEffectConsumer combines BlocBuilder and BlocEffectListener when the same bloc is used for both state-driven UI and transient effects. CubitWithEffects and BlocWithEffects implement its combined contract. A class using the Effects mixin directly can opt in by also implementing BlocEffectsSource<State, Effect>.

Effects are transient, non-buffered broadcast events. Every active listener receives each emission once. Emissions without active listeners are discarded, and new listeners do not receive past effects. Use one route-scoped listener for exclusive UI actions such as navigation and dialogs.

Libraries

bloc_effects