Before diving into complex state management with Bloc, it's essential to understand the core building blocks. This guide explains the key concepts: events, states, Cubit, Bloc, and the widgets that connect them to your UI.
The Three Pillars of Bloc
Events – Inputs to the Bloc (user actions, lifecycle events).
States – Outputs from the Bloc representing the current UI state.
Bloc/Cubit – The component that transforms events into states.
Events
Events are the way the UI communicates with the Bloc. They represent something that happened: a button press, a page load, a form submission. Events are typically defined as classes (often extending Equatable for easy comparison).
DARTRead-only
1
abstract classAuthEvent{}classLoginSubmittedextendsAuthEvent{
final String email;
final String password;LoginSubmitted(this.email,this.password);}classLogoutRequestedextendsAuthEvent{}
States
States represent the current condition of the UI. The Bloc emits states in response to events. UI widgets listen to states and rebuild accordingly.
DARTRead-only
1
abstract classAuthState{}classAuthInitialextendsAuthState{}classAuthLoadingextendsAuthState{}classAuthSuccessextendsAuthState{
final String userId;AuthSuccess(this.userId);}classAuthFailureextendsAuthState{
final String message;AuthFailure(this.message);}
Cubit (Simplified Bloc)
A Cubit is a class that extends Cubit<State> and uses methods to emit states. It’s the simplest way to use the BLoC pattern.
DARTRead-only
1
import'package:flutter_bloc/flutter_bloc.dart';classCounterState{
final int value;CounterState(this.value);}classCounterCubitextendsCubit<CounterState>{CounterCubit():super(CounterState(0));voidincrement()=>emit(CounterState(state.value +1));voiddecrement()=>emit(CounterState(state.value -1));}
Bloc (Event-Driven)
A Bloc extends Bloc<Event, State> and uses events and event handlers. It gives you more control, including event transformers (debounce, throttle) and advanced stream operations.
DARTRead-only
1
import'package:flutter_bloc/flutter_bloc.dart';
abstract classCounterEvent{}classIncrementextendsCounterEvent{}classDecrementextendsCounterEvent{}classCounterState{
final int value;CounterState(this.value);}classCounterBlocextendsBloc<CounterEvent, CounterState>{CounterBloc():super(CounterState(0)){
on<Increment>((event, emit)=>emit(CounterState(state.value +1)));
on<Decrement>((event, emit)=>emit(CounterState(state.value -1)));}}
Connecting UI with BlocProvider
BlocProvider is a widget that provides a Bloc or Cubit to its descendants. It also automatically disposes the bloc when the widget is removed from the tree.
BlocBuilder listens to a bloc and rebuilds when a new state is emitted. It requires you to specify the bloc type and the builder function that returns a widget based on the current state.
BlocListener is used for side effects like showing a snackbar, navigating to another screen, or logging. It does not rebuild the UI.
DARTRead-only
1
BlocListener<AuthBloc, AuthState>(listener:(context, state){if(state is AuthSuccess){
Navigator.pushReplacementNamed(context,'/home');}if(state is AuthFailure){
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content:Text(state.message)),);}},child:...);
Combining Builder and Listener with BlocConsumer
BlocConsumer combines BlocBuilder and BlocListener in one widget, useful when you need both rebuild and side effect handling.
DARTRead-only
1
BlocConsumer<AuthBloc, AuthState>(listener:(context, state){// handle side effects},builder:(context, state){// return UI based on state},);
Accessing the Bloc: context.read and context.watch
Inside a widget that has a BlocProvider ancestor, you can access the bloc using context.read<T>() (to call methods) and context.watch<T>() (to listen to state changes).
DARTRead-only
1
// Dispatch an event without rebuilding
context.read<AuthBloc>().add(LoginSubmitted(email, password));// Access state and rebuild when it changes
final state = context.watch<AuthBloc>().state;
Complete Example: Login Screen
Putting it all together: a simple login screen using Bloc.
DARTRead-only
1
classLoginPageextendsStatelessWidget{
final emailController =TextEditingController();
final passwordController =TextEditingController();
@override
Widget build(BuildContext context){returnScaffold(appBar:AppBar(title:Text('Login')),body: BlocConsumer<AuthBloc, AuthState>(listener:(context, state){if(state is AuthSuccess){
Navigator.pushReplacementNamed(context,'/home');}if(state is AuthFailure){
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content:Text(state.message)),);}},builder:(context, state){if(state is AuthLoading){returnCenter(child:CircularProgressIndicator());}returnPadding(padding:const EdgeInsets.all(16.0),child:Column(children:[TextField(controller: emailController,decoration:InputDecoration(labelText:'Email'),),TextField(controller: passwordController,decoration:InputDecoration(labelText:'Password'),obscureText:true,),SizedBox(height:20),ElevatedButton(onPressed:(){
context.read<AuthBloc>().add(LoginSubmitted(
emailController.text,
passwordController.text,),);},child:Text('Login'),),],),);},),);}}
Best Practices
Use Equatable – Simplify state/event equality checks to avoid unnecessary rebuilds.
Place BlocProvider at the appropriate level – Higher level for shared blocs, lower level for scoped ones.
Prefer context.read for event dispatching – Avoids unnecessary rebuilds.
Use BlocListener for side effects – Keep side effects out of BlocBuilder.
Keep states immutable – Always create new state objects; never mutate existing ones.
Separate business logic – Keep UI widgets free of business logic; put it in the bloc/cubit.
Common Mistakes
❌ Creating BlocProvider inside build – Recreates the bloc on every rebuild, losing state.
✅ Use BlocProvider at a higher level or outside build.
❌ Using BlocBuilder for side effects – Can cause infinite loops.
✅ Use BlocListener.
❌ Calling emit after bloc is closed – Throws an error.
✅ Check isClosed before emitting if needed.
❌ Not providing a bloc – context.read will throw an error.
✅ Ensure a BlocProvider exists higher in the tree.
Next Steps
Now that you understand the core concepts, explore:
Bloc’s core concepts—events, states, Cubit, Bloc, and the provider widgets—give you a solid foundation for building reactive Flutter apps. By understanding how they interact, you can create predictable, testable, and maintainable state management.
What widget should you use to rebuild the UI when the bloc state changes?
A
BlocProvider
B
BlocBuilder
C
BlocListener
D
BlocConsumer
Q2
of 3
Which method is used to dispatch an event to a Bloc?
A
context.bloc.add()
B
context.read<Bloc>().add()
C
Bloc.add()
D
emit()
Q3
of 3
What is the purpose of `BlocListener`?
A
To rebuild UI on state changes
B
To provide the bloc to the widget tree
C
To perform side effects (navigation, snackbars) without rebuilding
D
To combine builder and listener
Frequently Asked Questions
What is the difference between `context.read` and `context.watch`?
context.read<T>() returns the bloc instance without listening to state changes. Use it for dispatching events or calling methods. context.watch<T>() returns the bloc and also causes the widget to rebuild when the state changes. Prefer read for event dispatching to avoid unnecessary rebuilds.
When should I use `BlocConsumer` instead of separate `BlocBuilder` and `BlocListener`?
BlocConsumer is convenient when both rebuild and side effect logic are closely related. However, using separate widgets can improve readability when the builder and listener are large or reused.
Can I use Bloc without `BlocProvider`?
Yes, you can instantiate a bloc manually and pass it down manually, but BlocProvider simplifies dependency management and ensures proper disposal.
How do I handle errors in a bloc?
Catch exceptions inside event handlers or methods and emit an error state. For Bloc, you can also override onError to log or react to unhandled errors.
What is the purpose of `Equatable` in Bloc?
Equatable overrides == and hashCode for you, which helps Bloc avoid unnecessary rebuilds when states are identical but not the same instance. It's highly recommended.