Question #432MediumState ManagementImportant

In BLOC, what is the difference between BlocBuilder, BlocListener, and BlocConsumer? How to use them with examples along with BlocProvider and MultiBlocProvider?

#bloc#state-management#blocbuilder#bloclistener#blocconsumer#blocprovider

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

WidgetPurposeReturnsCalledUse Case
BlocBuilderRebuild UIWidgetMultiple timesDisplay data
BlocListenerSide effectsvoidOnce per stateNavigation, dialogs, snackbars
BlocConsumerBothWidgetMultiple + onceUI 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

dart
BlocBuilder<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

dart
import '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

dart
BlocBuilder<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

dart
BlocListener<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

dart
BlocListener<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

dart
BlocConsumer<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

dart
class 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

dart
BlocProvider(
  create: (context) => CounterCubit(),
  child: CounterScreen(),
)

With Dependencies

dart
BlocProvider(
  create: (context) => LoginBloc(
    authRepository: RepositoryProvider.of<AuthRepository>(context),
  ),
  child: LoginScreen(),
)

Lazy Loading (Default)

dart
BlocProvider(
  // BLoC created only when first accessed
  create: (context) => HeavyBloc(),
  lazy: true, // Default behavior
  child: MyScreen(),
)

Eager Loading

dart
BlocProvider(
  // 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)

dart
BlocProvider<AuthBloc>(
  create: (context) => AuthBloc(),
  child: BlocProvider<ThemeBloc>(
    create: (context) => ThemeBloc(),
    child: BlocProvider<SettingsBloc>(
      create: (context) => SettingsBloc(),
      child: MyApp(), // Deep nesting!
    ),
  ),
)

✅ With MultiBlocProvider (Flat)

dart
MultiBlocProvider(
  providers: [
    BlocProvider<AuthBloc>(
      create: (context) => AuthBloc(),
    ),
    BlocProvider<ThemeBloc>(
      create: (context) => ThemeBloc(),
    ),
    BlocProvider<SettingsBloc>(
      create: (context) => SettingsBloc(),
    ),
  ],
  child: MyApp(), // Clean and readable!
)

Real-World Example

dart
void 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

dart
class 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

WidgetReturnsCalledPurposeExample Use
BlocBuilderWidgetMultipleUI displayShow user profile
BlocListenervoidOnceSide effectsNavigate, show snackbar
BlocConsumerWidgetBothUI + side effectsLogin (show loading + navigate)
BlocProvider--Provide single BLoCInject BLoC
MultiBlocProvider--Provide multiple BLoCsApp-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