skip to content

With flutter_bloc, how do BlocBuilder and BlocListener differ, and which one should show a login-failure dialog?

level: juniorimportance: must knowfreq 76%

answer

  1. builder returns a widget
  2. builder may run many times
  3. listener returns void
  4. listener skips the initial state
  5. side effects belong in the listener

basics

~20 s

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

for a junior

Recall the split: BlocBuilder returns widgets for the current state, BlocListener runs a void callback for dialogs, snackbars and navigation.

for a middle

Explain when each callback runs: builder for the initial state, accepted states and ancestor rebuilds; listener once per change, never for the initial state.

for a senior

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.

for a principal

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