skip to content

With flutter_bloc, how do buildWhen and listenWhen filter updates, and what do their previous and current arguments refer to?

level: middleimportance: should knowfreq 55%

answer

  1. (previous, current) => bool
  2. default: always true
  3. previous is the last state received
  4. not the last state built
  5. ancestor rebuilds bypass buildWhen

basics

~20 s

Both are (previous, current) => bool filters run for every new state; false skips that rebuild or listener call. previous is the last state the widget received from the bloc, not the last one it built or reacted to.

solid answer

~50 s

`buildWhen` on `BlocBuilder` and `listenWhen` on `BlocListener` receive the previous and current state for each new state on the stream and default to `true`; returning `false` skips the rebuild or listener call for that state only. Two details matter. First, `previous` is the **last state the widget received**: flutter_bloc updates it on every state whether or not the filter passed, so `previous.status != current.status` reliably detects a transition. Second, `buildWhen` filters only stream updates: the initial build and any ancestor-driven rebuild still call `builder`, with the last state that passed the filter. So a `buildWhen` must compare every field the builder reads, or the UI goes stale. On the login screen, `buildWhen` compares email, password and the submitting flag, and `listenWhen` checks for a status transition into failure so the dialog opens once per failed attempt.

code

dart · 22 lines
dart
import 'package:equatable/equatable.dart';

enum LoginStatus { initial, submitting, failure, success }

class LoginState extends Equatable {
  const LoginState({
    this.email = '',
    this.password = '',
    this.status = LoginStatus.initial,
    this.errorMessage,
  });

  final String email;
  final String password;
  final LoginStatus status;
  final String? errorMessage;

  bool get isValid => email.contains('@') && password.length >= 8;

  @override
  List<Object?> get props => [email, password, status, errorMessage];
}

go deeper

for a junior

Recall that buildWhen and listenWhen take previous and current state, return a bool and default to true.

for a middle

Explain that previous is the last received state, updated even when the filter fails, and that the initial build and ancestor rebuilds bypass buildWhen.

for a senior

Show you write filters that compare exactly what the builder reads and transition checks for one-off reactions, and can diagnose stale UI or repeated dialogs from a bad filter.

for a principal

Decide when hand-written filters are worth their maintenance risk versus BlocSelector or smaller widgets, and make that a reviewable convention.

## The signatures In **flutter_bloc** 9.1 both filters share one shape, `bool Function(S previous, S current)`: - `BlocBuilder(buildWhen: ...)` — `true` means call `setState` with the new state and rebuild. - `BlocListener(listenWhen: ...)` — `true` means call the `listener` with the new state. - `BlocConsumer` takes both. Omitted, each defaults to `true`: every new state rebuilds or is listened to. ## When they run From the flutter_bloc source, a listening widget does this: 1. In `initState` it reads the bloc's current state and stores it as `previous`, then subscribes to the bloc's stream. 2. For every new state on the stream, it calls the filter with `(previous, state)`. 3. If the filter returns `true`, it runs the listener — for `BlocBuilder`, that listener is `setState` storing the new state. 4. It then sets `previous = state`, **whether or not the filter passed**. `BlocBuilder` is implemented as a `BlocListener` whose `listenWhen` is your `buildWhen`, so both follow exactly these rules. ## What previous means Take a login form whose `buildWhen` only passes on email changes: | Emitted | Filter called with | Result | `previous` afterwards | |---|---|---|---| | S1 email 'a' | (S0, S1) | true, rebuild | S1 | | S2 status submitting | (S1, S2) | false | S2 | | S3 status failure | (S2, S3) | false | S3 | When S3 arrives, `previous` is S2, a state the builder never built. That is what makes transition checks work: `previous.status != current.status && current.status == LoginStatus.failure` is `true` exactly once per failed attempt, regardless of what the filter decided earlier. ## buildWhen does not control every build - The **initial build** uses the bloc's state without consulting `buildWhen`. - An **ancestor rebuild** calls `builder` with the last state that passed `buildWhen`, not the bloc's latest state. Consequence: if `buildWhen` ignores a field the builder reads, the widget shows an old value for that field until some other change passes the filter. The rule is simple — **compare every field the builder uses, and nothing else**. ## The login screen ```dart BlocBuilder<LoginCubit, LoginState>( buildWhen: (previous, current) => previous.email != current.email || previous.password != current.password || (previous.status == LoginStatus.submitting) != (current.status == LoginStatus.submitting), builder: (context, state) => LoginForm( email: state.email, password: state.password, submitting: state.status == LoginStatus.submitting, ), ) ``` The builder reads three things, and `buildWhen` compares exactly those three. The dialog's `BlocListener` uses the transition check from the previous section. ## Pitfalls - `listenWhen: (p, c) => c.status == LoginStatus.failure` fires for **every** state while the status stays failure, so each keystroke that emits a new state reopens the dialog. - A `buildWhen` that omits a field the builder reads produces stale UI that only appears after unrelated rebuilds. - Filters run for every state: keep them cheap and free of side effects. - A filter never sees a state the bloc dropped as a duplicate; that happens inside the bloc before any widget is involved. - When one derived value is all the widget needs, `BlocSelector` replaces a hand-written `buildWhen` and removes the stale-field risk.

  • Can buildWhen stop a BlocBuilder from building when its parent rebuilds?
    No. buildWhen filters states arriving from the bloc only. When an ancestor rebuilds, the BlocBuilder's build runs and calls builder with the last state that passed buildWhen. To avoid parent-driven rebuild cost, restructure the tree or use const children — buildWhen is not that tool.
  • How would you show a snackbar only when an error message changes to a new non-null value?
    Use `listenWhen: (p, c) => c.errorMessage != null && p.errorMessage != c.errorMessage`. Because `previous` is the last state received, the check fires once per new message and stays quiet while the user edits fields that leave the message unchanged.

saying these in an interview costs you the question

  • previous is the last state the builder actually built
  • buildWhen also prevents rebuilds triggered by a parent widget
  • listenWhen defaults to false, so listeners must opt in
  • A buildWhen may skip fields the builder reads without any visible effect
  • listenWhen can safely trigger navigation itself