flutter_bloc Widgets: BlocListener and MultiBlocProvider
flutter_bloc Widgets: BlocListener and MultiBlocProvider
The flutter_bloc package ships with a family of purpose-built widgets that cover every interaction pattern you need when working with BLoCs. In this lesson we focus on the three that are most commonly misunderstood: BlocListener, BlocConsumer, and MultiBlocProvider. Understanding why each widget exists is just as important as knowing how to use it.
BlocListener — Side-Effect-Only Reactions
BlocListener is designed for one-time side effects that should never trigger a UI rebuild: navigation pushes, ScaffoldMessenger.showSnackBar, dialog popups, or analytics events. Its listener callback fires whenever the bloc emits a new state, but it renders no widget of its own — it simply wraps a child.
BlocBuilder. If it should trigger a side effect with no rebuild, use BlocListener. Mixing side effects inside BlocBuilder's builder callback is a common anti-pattern that causes duplicate calls.BlocListener — Navigation and SnackBar Example
BlocListener<AuthBloc, AuthState>(
// Optional: only fire when this condition is true
listenWhen: (previous, current) =>
previous.status != current.status,
listener: (context, state) {
if (state.status == AuthStatus.authenticated) {
// Navigate without rebuilding the current widget
Navigator.of(context).pushReplacementNamed('/home');
} else if (state.status == AuthStatus.error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'Login failed')),
);
}
},
child: const LoginFormWidget(),
)
The optional listenWhen predicate mirrors BlocBuilder's buildWhen: it receives the previous and current state and should return true only when the listener should fire. Use it to avoid reacting to every single emission when you only care about a specific field transition.
BlocConsumer — Build and Listen Together
BlocConsumer is a convenience widget that combines BlocBuilder and BlocListener into a single widget when you need both a UI rebuild and a side effect in response to the same state change. It accepts builder, listener, buildWhen, and listenWhen.
BlocConsumer — Form Submission with Spinner and SnackBar
BlocConsumer<RegistrationBloc, RegistrationState>(
listenWhen: (prev, curr) => curr.isFailure || curr.isSuccess,
listener: (context, state) {
if (state.isSuccess) {
Navigator.of(context).pushReplacementNamed('/dashboard');
}
if (state.isFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage),
backgroundColor: Colors.red,
),
);
}
},
buildWhen: (prev, curr) => prev.isSubmitting != curr.isSubmitting,
builder: (context, state) {
return ElevatedButton(
onPressed: state.isSubmitting
? null
: () => context
.read<RegistrationBloc>()
.add(const FormSubmitted()),
child: state.isSubmitting
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Register'),
);
},
)
MultiBlocProvider — Composing BLoCs at the App Root
Real applications have many BLoCs. Nesting multiple BlocProviders creates a deeply indented, hard-to-read widget tree. MultiBlocProvider flattens that structure by accepting a providers list. Each entry is a BlocProvider, and they are created in list order. The result is identical to nesting them manually, but far more readable.
MultiBlocProvider at the App Root
// main.dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>(
create: (context) => AuthBloc(
authRepository: context.read<AuthRepository>(),
)..add(const AppStarted()),
),
BlocProvider<ThemeBloc>(
create: (context) => ThemeBloc(
settingsRepository: context.read<SettingsRepository>(),
),
),
BlocProvider<CartBloc>(
create: (context) => CartBloc(
cartRepository: context.read<CartRepository>(),
),
),
],
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, themeState) => MaterialApp(
themeMode: themeState.mode,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
home: const AppRouter(),
),
),
);
}
}
MultiBlocProvider near the root are singletons for the widget-tree subtree below them. Avoid creating heavyweight BLoCs high in the tree if they are only needed on one screen. Prefer scoped BlocProvider wrappers at the route level for screen-specific BLoCs.MultiBlocListener and MultiRepositoryProvider
The same "Multi" pattern extends to listeners and repository injection. MultiBlocListener replaces nested BlocListener widgets at a router level. MultiRepositoryProvider injects plain Dart service/repository objects (no BLoC) into the tree so they can be read with context.read<T>().
Summary
- BlocListener — side effects only (navigation, snackbars, dialogs); never rebuilds.
- BlocConsumer — combined rebuild + side effect; use when both are needed for the same emission.
- MultiBlocProvider — flat, readable composition of many
BlocProviders; ideal at the app or route root. - Use
listenWhen/buildWhento filter emissions and prevent unnecessary work.