With flutter_bloc, how do BlocBuilder and BlocListener differ, and which one should show a login-failure dialog?
answer
- builder returns a widget
- builder may run many times
- listener returns void
- listener skips the initial state
- side effects belong in the listener
basics
~20 sBlocBuilder rebuilds part of the UI from the current state and may run its builder many times, so it must be pure. BlocListener runs a void callback once per state change, never for the initial state, so dialogs, snackbars and navigation belong there.
solid answer
~50 s`BlocBuilder<LoginCubit, LoginState>` builds from the bloc's current `state` straight away and again after each accepted new state; its `builder` also reruns whenever an ancestor rebuilds it, so the docs require it to be a pure function of the state — no `showDialog`, no `add` calls. `BlocListener<LoginCubit, LoginState>` subscribes to the bloc's stream and calls its `void` `listener` once for each state change, not for the initial state, and simply returns its `child`, so it never rebuilds anything itself. On a login screen the form is a `BlocBuilder` and the failure dialog lives in a `BlocListener` wrapped around it: a dialog shown from a builder would reappear on unrelated rebuilds. Since flutter_bloc 9.0 the listener also checks that the widget is still mounted before running. Both find the bloc through the nearest `BlocProvider` unless you pass `bloc:` explicitly.
code
dart · 28 linesimport 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// LoginCubit, LoginState, LoginStatus and LoginForm are declared elsewhere.
class LoginView extends StatelessWidget {
const LoginView({super.key});
@override
Widget build(BuildContext context) {
return BlocListener<LoginCubit, LoginState>(
listenWhen: (previous, current) =>
previous.status != current.status &&
current.status == LoginStatus.failure,
listener: (context, state) {
showDialog<void>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Sign-in failed'),
content: Text(state.errorMessage ?? 'Please try again.'),
),
);
},
child: BlocBuilder<LoginCubit, LoginState>(
builder: (context, state) => LoginForm(state: state),
),
);
}
}go deeper
Recall the split: BlocBuilder returns widgets for the current state, BlocListener runs a void callback for dialogs, snackbars and navigation.
Explain when each callback runs: builder for the initial state, accepted states and ancestor rebuilds; listener once per change, never for the initial state.
Show you can spot side effects hiding in builders during review and explain the concrete symptoms they cause, such as repeated dialogs after a theme change.
Set a team rule that builders stay pure and all one-off reactions live in listeners, so screen behaviour never depends on how often Flutter rebuilds.
## Two jobs: render and react The **flutter_bloc** package (9.1) connects a `Bloc` or `Cubit` from the **bloc** package to the widget tree. Its two core widgets split UI work into two kinds: - **Rendering** — describing what the screen looks like for a given state. That is `BlocBuilder`. - **Reacting** — doing something once when the state changes: showing a dialog or a snackbar, navigating, triggering haptics. That is `BlocListener`. Mixing the two is the most common flutter_bloc mistake interviewers probe for. ## BlocBuilder - Takes a `builder: (context, state) => Widget` and an optional `buildWhen`. - Reads the bloc's current `state` in `initState` and builds with it immediately — the initial state **is** rendered. - Subscribes to the bloc's stream and calls `setState` for each new state that passes `buildWhen`. - Its `build` also runs whenever an ancestor rebuilds it, and then calls `builder` with the last state it accepted. - Because of all that, the bloc docs say `builder` may be called many times and should be a **pure function** of the state. ## BlocListener - Takes a `listener: (context, state) { ... }` returning `void`, an optional `listenWhen`, and a `child`. - Subscribes in `initState` and remembers the current state as the starting `previous`. - Calls `listener` **once per state change**, and **not** for the initial state. - Returns its `child` unchanged: it never rebuilds on state changes. - Since flutter_bloc 9.0 it skips the call if the widget is no longer mounted. ## Side by side | | `BlocBuilder` | `BlocListener` | |---|---|---| | Callback returns | a `Widget` | `void` | | Runs for the initial state | yes | no | | Runs on ancestor rebuilds | yes | no | | Filter | `buildWhen` | `listenWhen` | | Use for | UI that depends on state | dialogs, snackbars, navigation | ## The login screen The scenario: a failed sign-in must show a dialog, while only the form rebuilds as the user types. ```dart BlocListener<LoginCubit, LoginState>( listenWhen: (previous, current) => previous.status != current.status && current.status == LoginStatus.failure, listener: (context, state) { showDialog<void>( context: context, builder: (_) => AlertDialog( title: const Text('Sign-in failed'), content: Text(state.errorMessage ?? 'Please try again.'), ), ); }, child: BlocBuilder<LoginCubit, LoginState>( builder: (context, state) => LoginForm(state: state), ), ) ``` The listener wraps the builder, so the dialog logic sits at the page level while the builder owns the rendering of the form. ## Why a dialog inside a builder is a bug 1. The builder runs for the **initial state**: a screen created while the state is already a failure would open the dialog immediately. 2. It runs on **ancestor rebuilds**, for example after a theme change or a parent's `setState`, reopening the dialog with no new failure. 3. It runs on **every accepted state**: typing into the form after a failure would open the dialog per keystroke if the failure status is still set. 4. A build method is meant to describe UI, not to change navigation; side effects there make the screen depend on how often Flutter happens to build. A listener has none of these problems: it is tied to state **changes**, not to builds. ## Explicit bloc parameter Both widgets look the bloc up from the nearest `BlocProvider` when `bloc:` is omitted. The docs reserve an explicit `bloc:` for an instance that is not reachable through a provider, such as one scoped to a single widget.
- Why does a BlocListener not fire for the state the bloc already has when the listener is created?It records the current state as its starting `previous` and only reacts to states arriving on the stream afterwards. A one-off reaction such as a dialog should be triggered by a change, not by whatever state happened to exist when the widget was built. If the UI must reflect the current state, that is a builder's job.
- What changed for BlocListener in flutter_bloc 9.0?The listener now checks `mounted` before running, so a state arriving after the widget left the tree no longer invokes the callback with a defunct `BuildContext`. It removes a class of 'looking up a deactivated widget's ancestor' errors in listeners that show dialogs or snackbars.
saying these in an interview costs you the question
- BlocListener's listener also runs for the initial state
- Showing a dialog from BlocBuilder's builder is fine if you check the state
- BlocListener rebuilds its child on every state change
- BlocBuilder's builder runs only when the bloc emits a new state
- A listener must return the widget to display