In BLOC, what is the difference between BlocBuilder, BlocListener, and BlocConsumer? How to use them with examples along with BlocProvider and MultiBlocProvider?
Answer
Overview
In Flutter BLOC, there are three main widgets for consuming BLoC state: BlocBuilder, BlocListener, and BlocConsumer. Each serves a different purpose, and understanding when to use each is crucial for clean, maintainable code.
Quick Comparison
| Widget | Purpose | Returns | Called | Use Case |
|---|---|---|---|---|
| BlocBuilder | Rebuild UI | Widget | Multiple times | Display data |
| BlocListener | Side effects | void | Once per state | Navigation, dialogs, snackbars |
| BlocConsumer | Both | Widget | Multiple + once | UI rebuild + side effects |
1. BlocBuilder - Rebuild UI
What It Does
BlocBuilder rebuilds the widget tree in response to state changes. It's used when you want to display something based on the current state.
Signature
dartBlocBuilder<BlocType, StateType>( bloc: myBloc, // Optional if provided via BlocProvider buildWhen: (previous, current) => ..., // Optional condition builder: (context, state) { // Return widget based on state return Widget(); }, )
Example - Counter
dartimport 'package:flutter_bloc/flutter_bloc.dart'; // BLoC class CounterCubit extends Cubit<int> { CounterCubit() : super(0); void increment() => emit(state + 1); void decrement() => emit(state - 1); } // UI with BlocBuilder class CounterScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('BlocBuilder Example')), body: Center( child: BlocBuilder<CounterCubit, int>( builder: (context, count) { // This rebuilds every time count changes return Text( 'Count: $count', style: TextStyle(fontSize: 48), ); }, ), ), floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton( onPressed: () => context.read<CounterCubit>().increment(), child: Icon(Icons.add), ), SizedBox(height: 8), FloatingActionButton( onPressed: () => context.read<CounterCubit>().decrement(), child: Icon(Icons.remove), ), ], ), ); } }
buildWhen - Optimize Rebuilds
dartBlocBuilder<CounterCubit, int>( buildWhen: (previous, current) { // Only rebuild if count is even return current % 2 == 0; }, builder: (context, count) { return Text('Even count: $count'); }, )
2. BlocListener - Side Effects
What It Does
BlocListener executes code once per state change. It's used for one-time actions like navigation, showing dialogs, or displaying snackbars.
Signature
dartBlocListener<BlocType, StateType>( bloc: myBloc, // Optional listenWhen: (previous, current) => ..., // Optional condition listener: (context, state) { // Perform side effects (void function) }, child: Widget(), )
Example - Login with Navigation
dart// States abstract class LoginState {} class LoginInitial extends LoginState {} class LoginLoading extends LoginState {} class LoginSuccess extends LoginState { final User user; LoginSuccess(this.user); } class LoginFailure extends LoginState { final String error; LoginFailure(this.error); } // BLoC class LoginBloc extends Bloc<LoginEvent, LoginState> { LoginBloc() : super(LoginInitial()) { on<LoginSubmitted>((event, emit) async { emit(LoginLoading()); try { final user = await authRepository.login(event.email, event.password); emit(LoginSuccess(user)); } catch (e) { emit(LoginFailure(e.toString())); } }); } } // UI with BlocListener class LoginScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('BlocListener Example')), body: BlocListener<LoginBloc, LoginState>( listener: (context, state) { // ✅ Side effects - executed ONCE per state change if (state is LoginSuccess) { // Navigate to home Navigator.pushReplacementNamed(context, '/home'); } else if (state is LoginFailure) { // Show error snackbar ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(state.error)), ); } }, child: LoginForm(), // Child doesn't rebuild on state change ), ); } }
listenWhen - Conditional Listening
dartBlocListener<LoginBloc, LoginState>( listenWhen: (previous, current) { // Only listen when transitioning from loading to success/failure return previous is LoginLoading && (current is LoginSuccess || current is LoginFailure); }, listener: (context, state) { // Only called when condition is true }, child: LoginForm(), )
3. BlocConsumer - UI Rebuild + Side Effects
What It Does
BlocConsumer combines BlocBuilder and BlocListener. Use it when you need both UI updates and side effects.
Signature
dartBlocConsumer<BlocType, StateType>( bloc: myBloc, // Optional listenWhen: (previous, current) => ..., // Optional buildWhen: (previous, current) => ..., // Optional listener: (context, state) { // Side effects (void) }, builder: (context, state) { // Return widget return Widget(); }, )
Example - Login with UI + Navigation
dartclass LoginScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('BlocConsumer Example')), body: BlocConsumer<LoginBloc, LoginState>( // Side effects listener: (context, state) { if (state is LoginSuccess) { Navigator.pushReplacementNamed(context, '/home'); } else if (state is LoginFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(state.error)), ); } }, // UI rebuilds builder: (context, state) { if (state is LoginLoading) { return Center(child: CircularProgressIndicator()); } return Padding( padding: EdgeInsets.all(16), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextField( decoration: InputDecoration(labelText: 'Email'), ), SizedBox(height: 16), TextField( decoration: InputDecoration(labelText: 'Password'), obscureText: true, ), SizedBox(height: 24), ElevatedButton( onPressed: () { context.read<LoginBloc>().add( LoginSubmitted(email: '...', password: '...'), ); }, child: Text('Login'), ), ], ), ); }, ), ); } }
When to Use Each
Use BlocBuilder When:
dart✅ You only need to rebuild UI based on state ✅ No side effects needed ✅ Example: Displaying a list, showing user profile, counter display
Use BlocListener When:
dart✅ You only need side effects (no UI changes) ✅ Navigation, dialogs, snackbars ✅ Example: Auto-logout on token expiry, showing error messages
Use BlocConsumer When:
dart✅ You need BOTH UI rebuild AND side effects ✅ Example: Login screen (show loading + navigate on success) ✅ Form submission (show progress + show error toast)
4. BlocProvider - Dependency Injection
What It Does
BlocProvider creates and provides a BLoC to the widget tree below it.
Single BlocProvider
dartBlocProvider( create: (context) => CounterCubit(), child: CounterScreen(), )
With Dependencies
dartBlocProvider( create: (context) => LoginBloc( authRepository: RepositoryProvider.of<AuthRepository>(context), ), child: LoginScreen(), )
Lazy Loading (Default)
dartBlocProvider( // BLoC created only when first accessed create: (context) => HeavyBloc(), lazy: true, // Default behavior child: MyScreen(), )
Eager Loading
dartBlocProvider( // BLoC created immediately create: (context) => AuthBloc()..add(CheckAuthStatus()), lazy: false, child: App(), )
5. MultiBlocProvider - Multiple BLoCs
What It Does
MultiBlocProvider provides multiple BLoCs to avoid deep nesting.
❌ Without MultiBlocProvider (Nested)
dartBlocProvider<AuthBloc>( create: (context) => AuthBloc(), child: BlocProvider<ThemeBloc>( create: (context) => ThemeBloc(), child: BlocProvider<SettingsBloc>( create: (context) => SettingsBloc(), child: MyApp(), // Deep nesting! ), ), )
✅ With MultiBlocProvider (Flat)
dartMultiBlocProvider( providers: [ BlocProvider<AuthBloc>( create: (context) => AuthBloc(), ), BlocProvider<ThemeBloc>( create: (context) => ThemeBloc(), ), BlocProvider<SettingsBloc>( create: (context) => SettingsBloc(), ), ], child: MyApp(), // Clean and readable! )
Real-World Example
dartvoid main() { runApp( MultiBlocProvider( providers: [ // Global BLoCs BlocProvider<AuthBloc>( create: (context) => AuthBloc(AuthRepository()), lazy: false, // Check auth immediately ), BlocProvider<ThemeBloc>( create: (context) => ThemeBloc(), ), BlocProvider<LocaleBloc>( create: (context) => LocaleBloc(), ), ], child: MyApp(), ), ); } class MyApp extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder<ThemeBloc, ThemeState>( builder: (context, themeState) { return MaterialApp( theme: themeState.themeData, home: BlocBuilder<AuthBloc, AuthState>( builder: (context, authState) { if (authState is Authenticated) { return HomeScreen(); } return LoginScreen(); }, ), ); }, ); } }
Complete Real-World Example
BLoC Setup
dart// Events abstract class TodoEvent {} class LoadTodos extends TodoEvent {} class AddTodo extends TodoEvent { final String title; AddTodo(this.title); } class ToggleTodo extends TodoEvent { final int id; ToggleTodo(this.id); } class DeleteTodo extends TodoEvent { final int id; DeleteTodo(this.id); } // States abstract class TodoState {} class TodoInitial extends TodoState {} class TodoLoading extends TodoState {} class TodoLoaded extends TodoState { final List<Todo> todos; TodoLoaded(this.todos); } class TodoError extends TodoState { final String message; TodoError(this.message); } // BLoC class TodoBloc extends Bloc<TodoEvent, TodoState> { final TodoRepository repository; TodoBloc(this.repository) : super(TodoInitial()) { on<LoadTodos>(_onLoadTodos); on<AddTodo>(_onAddTodo); on<ToggleTodo>(_onToggleTodo); on<DeleteTodo>(_onDeleteTodo); } Future<void> _onLoadTodos(LoadTodos event, Emitter<TodoState> emit) async { emit(TodoLoading()); try { final todos = await repository.fetchTodos(); emit(TodoLoaded(todos)); } catch (e) { emit(TodoError(e.toString())); } } Future<void> _onAddTodo(AddTodo event, Emitter<TodoState> emit) async { if (state is TodoLoaded) { try { final newTodo = await repository.addTodo(event.title); final currentTodos = (state as TodoLoaded).todos; emit(TodoLoaded([...currentTodos, newTodo])); } catch (e) { emit(TodoError(e.toString())); } } } Future<void> _onToggleTodo(ToggleTodo event, Emitter<TodoState> emit) async { if (state is TodoLoaded) { final currentTodos = (state as TodoLoaded).todos; final updatedTodos = currentTodos.map((todo) { return todo.id == event.id ? todo.copyWith(completed: !todo.completed) : todo; }).toList(); emit(TodoLoaded(updatedTodos)); } } Future<void> _onDeleteTodo(DeleteTodo event, Emitter<TodoState> emit) async { if (state is TodoLoaded) { final currentTodos = (state as TodoLoaded).todos; emit(TodoLoaded(currentTodos.where((t) => t.id != event.id).toList())); } } }
UI Implementation
dartclass TodoScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Todos')), body: BlocConsumer<TodoBloc, TodoState>( // Side effects (listener) listener: (context, state) { if (state is TodoError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: Colors.red, ), ); } }, // UI rebuilds (builder) builder: (context, state) { if (state is TodoInitial) { return Center( child: ElevatedButton( onPressed: () => context.read<TodoBloc>().add(LoadTodos()), child: Text('Load Todos'), ), ); } if (state is TodoLoading) { return Center(child: CircularProgressIndicator()); } if (state is TodoLoaded) { return ListView.builder( itemCount: state.todos.length, itemBuilder: (context, index) { final todo = state.todos[index]; return ListTile( leading: Checkbox( value: todo.completed, onChanged: (_) { context.read<TodoBloc>().add(ToggleTodo(todo.id)); }, ), title: Text( todo.title, style: TextStyle( decoration: todo.completed ? TextDecoration.lineThrough : null, ), ), trailing: IconButton( icon: Icon(Icons.delete), onPressed: () { context.read<TodoBloc>().add(DeleteTodo(todo.id)); }, ), ); }, ); } return Center(child: Text('Something went wrong')); }, ), floatingActionButton: FloatingActionButton( onPressed: () => _showAddTodoDialog(context), child: Icon(Icons.add), ), ); } void _showAddTodoDialog(BuildContext context) { showDialog( context: context, builder: (_) => BlocProvider.value( value: context.read<TodoBloc>(), child: AddTodoDialog(), ), ); } }
Best Practices
1. Use BlocProvider.value for Existing BLoCs
dart// ❌ Wrong - Creates new BLoC instance Navigator.push( context, MaterialPageRoute( builder: (_) => BlocProvider( create: (_) => CounterCubit(), // New instance! child: DetailScreen(), ), ), ); // ✅ Correct - Uses existing BLoC Navigator.push( context, MaterialPageRoute( builder: (_) => BlocProvider.value( value: context.read<CounterCubit>(), // Same instance child: DetailScreen(), ), ), );
2. Use context.read() for Events
dart// ✅ Good - Don't listen, just dispatch ElevatedButton( onPressed: () => context.read<CounterCubit>().increment(), child: Text('Increment'), ) // ❌ Bad - Unnecessary listening ElevatedButton( onPressed: () => context.watch<CounterCubit>().increment(), child: Text('Increment'), )
3. Use buildWhen and listenWhen
dart// ✅ Optimize rebuilds BlocBuilder<UserBloc, UserState>( buildWhen: (previous, current) => previous.user != current.user, builder: (context, state) => ..., )
Summary
| Widget | Returns | Called | Purpose | Example Use |
|---|---|---|---|---|
| BlocBuilder | Widget | Multiple | UI display | Show user profile |
| BlocListener | void | Once | Side effects | Navigate, show snackbar |
| BlocConsumer | Widget | Both | UI + side effects | Login (show loading + navigate) |
| BlocProvider | - | - | Provide single BLoC | Inject BLoC |
| MultiBlocProvider | - | - | Provide multiple BLoCs | App-level BLoCs |
Key Takeaways
BlocBuilder for UI updates
BlocListener for one-time side effects
BlocConsumer when you need both
BlocProvider to inject BLoC
MultiBlocProvider to avoid nesting
Resources
Additional Resources
- https://bloclibrary.dev/flutter-bloc-concepts/
- https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocBuilder-class.html
- https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocListener-class.html
- https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocConsumer-class.html
- https://pub.dev/packages/flutter_bloc