With flutter_bloc, when should you use BlocConsumer rather than a BlocListener wrapping a BlocBuilder, and how does it run its callbacks?
answer
- builder plus listener, one bloc
- both filters optional
- listener runs before the rebuild
- separate widgets scope differently
- MultiBlocListener for several blocs
basics
~20 sBlocConsumer combines a builder and a listener for one bloc, with optional buildWhen and listenWhen, and behaves like a BlocListener around a BlocBuilder. Use it when both reactions belong to the same widget; keep them separate when the listener should span a larger subtree.
solid answer
~40 s`BlocConsumer<LoginCubit, LoginState>` takes `builder`, `listener`, and optional `buildWhen`, `listenWhen` and `bloc`. The docs describe it as a nested `BlocListener` and `BlocBuilder` with less boilerplate, to be used only when you need both to rebuild and to react. Internally it is a `BlocBuilder` whose `buildWhen` first evaluates `listenWhen` and calls the listener, then returns your `buildWhen` — so for each new state the listener runs before the rebuild and both filters see the same `previous`. As with BlocListener, the listener never runs for the initial state. Prefer separate widgets when the listener should wrap the whole page but only a small part rebuilds — a page-level listener for the failure dialog and a narrow `BlocBuilder` around the submit button — and use `MultiBlocListener` when several blocs drive side effects.
code
dart · 30 linesimport 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// LoginCubit, LoginState, LoginStatus, LoginFields and SubmitButton are declared elsewhere.
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return BlocListener<LoginCubit, LoginState>(
listenWhen: (p, c) =>
p.status != c.status && c.status == LoginStatus.failure,
listener: (context, state) => showDialog<void>(
context: context,
builder: (_) => const AlertDialog(content: Text('Sign-in failed')),
),
child: Column(
children: [
const LoginFields(),
BlocBuilder<LoginCubit, LoginState>(
buildWhen: (p, c) => p.status != c.status,
builder: (context, state) => SubmitButton(
busy: state.status == LoginStatus.submitting,
),
),
],
),
);
}
}go deeper
Recall that BlocConsumer is a builder and a listener for one bloc in a single widget, with optional buildWhen and listenWhen.
Explain its internals: the listener runs inside the builder's filter, before the rebuild, both filters share previous, and the initial state skips the listener.
Show judgement on rebuild scope: when a consumer is tidy and when a high BlocListener plus a narrow builder or MultiBlocListener is the better structure.
Set screen-structure conventions for listeners and builders so rebuild scope and side-effect placement stay consistent across features.
## What BlocConsumer is **flutter_bloc** 9.1 offers `BlocConsumer` for the case where one widget must both **render** from a bloc's state and **react** to its changes. Its constructor takes: - `builder` — required, `(context, state) => Widget`; - `listener` — required, `(context, state) { ... }` returning `void`; - `buildWhen` and `listenWhen` — optional `(previous, current) => bool` filters, each defaulting to `true`; - `bloc` — optional; otherwise the nearest `BlocProvider` supplies it. The bloc docs describe it as analogous to a nested `BlocListener` and `BlocBuilder`, and say it should only be used when you need both. ## How it runs The source makes the behaviour precise. `BlocConsumer` builds a single `BlocBuilder` and passes it a `buildWhen` that does two things: 1. It calls your `listenWhen(previous, current)`, and if that is `true` it calls your `listener(context, current)`. 2. It returns your `buildWhen(previous, current)` — `true` rebuilds with the new state. Consequences: - For each new state, the **listener runs first**, synchronously, and the rebuild follows. - Both filters receive the **same** `previous`: the last state the widget received. - The **initial state** reaches the builder but never the listener, because no filter runs for it. - The listener runs inside the builder's filter step, so it must stay quick; long work belongs in the bloc. ## Consumer or separate widgets? | Need | Better choice | Why | |---|---|---| | React and rebuild the same small widget | `BlocConsumer` | one widget, less nesting | | React at page level, rebuild a small part | `BlocListener` + narrow `BlocBuilder` | rebuild scope stays small | | React to several blocs | `MultiBlocListener` | flat list instead of nesting | | Rebuild only | `BlocBuilder` or `BlocSelector` | no listener needed | | React only | `BlocListener` | nothing rebuilds | The key question is **scope**. A `BlocConsumer`'s builder rebuilds everything it returns. If you wrap a whole login page in a consumer just to show a dialog, every accepted state rebuilds the page. Splitting lets the listener sit high while the builder hugs the widgets that change. ## The login screen, both ways With a consumer around the form: ```dart BlocConsumer<LoginCubit, LoginState>( listenWhen: (p, c) => p.status != c.status && c.status == LoginStatus.failure, listener: (context, state) => showDialog<void>( context: context, builder: (_) => const AlertDialog(content: Text('Sign-in failed')), ), buildWhen: (p, c) => p.status != c.status, builder: (context, state) => SubmitButton( busy: state.status == LoginStatus.submitting, ), ) ``` This works well when the consumer wraps just the button. If the dialog should also cover other widgets on the page, move the listener up as a `BlocListener` and leave a `BlocBuilder` around the button. ## MultiBlocListener When a page reacts to several blocs — a failure dialog from `LoginCubit` and a banner from a connectivity cubit — nesting listeners gets deep. `MultiBlocListener(listeners: [...], child: ...)` flattens them. As with `MultiBlocProvider`, any `child` given to a listener inside the list is ignored; the single `child` of `MultiBlocListener` is what renders. ## Pitfalls 1. Using a consumer when only one of the callbacks is needed; the docs call it out as the case where it should not be used. 2. Expecting the listener to fire for the state present when the screen opens. 3. Putting heavy or asynchronous work in the listener instead of adding an event or calling a method on the bloc.
- Does BlocConsumer's listener run when its buildWhen returns false?Yes, if listenWhen passes. The consumer's internal filter calls listenWhen and the listener first, then returns your buildWhen result; a false buildWhen only skips the rebuild. The two decisions are independent, which is why the consumer can react without rebuilding.
- Why can a BlocConsumer wrapping a whole page hurt performance?Its builder returns the entire page, so every state that passes buildWhen rebuilds all of it, even if only a button changes. A page-level BlocListener with a small BlocBuilder or BlocSelector keeps reactions high and rebuilds narrow.
saying these in an interview costs you the question
- BlocConsumer's listener runs after the frame the builder produces
- BlocConsumer also calls the listener for the initial state
- BlocConsumer creates and provides the bloc it consumes
- Wrapping a whole page in BlocConsumer has no rebuild cost
- listenWhen and buildWhen in a consumer see different previous states