SingleLiveEvent pattern implementation for Cubit and 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.