skip to content

With flutter_bloc, what does BlocSelector do, and when is it a better fit than BlocBuilder with buildWhen?

level: middleimportance: nice to knowfreq 28%

answer

  1. selector maps state to one value
  2. builder gets the selected value
  3. rebuilds only when it changes
  4. compared with ==
  5. selected value must be immutable

basics

~20 s

BlocSelector maps each state to one value with its selector and rebuilds only when that value changes, compared with ==. It suits a widget that depends on a single derived value, such as a submit button that needs only whether the form is valid.

solid answer

~50 s

`BlocSelector<LoginCubit, LoginState, bool>(selector: (state) => state.isValid, builder: (context, isValid) => ...)` runs `selector` for every new state, compares the result with the previously selected value, and calls `setState` only if they differ. The `builder` receives the selected value, not the whole state, so it cannot read a field it does not rebuild for — the stale-UI trap of a hand-written `buildWhen` that forgets a field. There is no `buildWhen` parameter; the comparison is the filter. The docs require the selected value to be immutable, and it needs value equality: a fresh plain object or `List` compares unequal every time and rebuilds on every state. Use `BlocBuilder` with `buildWhen` when the builder needs several fields or the whole state; `BlocSelector` for one derived value, or a record of a few. flutter_bloc 9.1.1 made it re-select when the `selector` function itself changes.

code

dart · 25 lines
dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// LoginCubit (with submit()), LoginState (with isValid) and LoginStatus are declared elsewhere.
class SubmitButton extends StatelessWidget {
  const SubmitButton({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocSelector<LoginCubit, LoginState, ({bool valid, bool submitting})>(
      selector: (state) => (
        valid: state.isValid,
        submitting: state.status == LoginStatus.submitting,
      ),
      builder: (context, s) => FilledButton(
        onPressed: s.valid && !s.submitting
            ? () => context.read<LoginCubit>().submit()
            : null,
        child: s.submitting
            ? const CircularProgressIndicator()
            : const Text('Sign in'),
      ),
    );
  }
}

go deeper

for a junior

Recall that BlocSelector picks one value from the state and rebuilds only when that value changes.

for a middle

Explain the != comparison, why the selected value needs value equality and immutability, and why the builder receiving T prevents stale fields.

for a senior

Show you choose between BlocSelector and buildWhen per widget and can diagnose a selector that rebuilds every time or never.

for a principal

Weigh fine-grained selectors against simpler widget splits when setting rebuild-scope conventions for large screens.

## What it is `BlocSelector<B, S, T>` in **flutter_bloc** 9.1 is a builder that narrows a bloc's state `S` to one value `T` before building. The bloc docs call it analogous to `BlocBuilder`, but filtering updates by a selected value: unnecessary builds are prevented when the selected value does not change. Its parameters: - `selector: (S state) => T` — required; - `builder: (BuildContext context, T value) => Widget` — required, and note it receives `T`, not `S`; - `bloc` — optional, otherwise looked up from the nearest `BlocProvider`. ## How it works From the flutter_bloc source: 1. In `initState` it runs `selector` on the bloc's current state and stores the result. 2. It listens to the bloc's states. For each one it runs `selector` again. 3. If the new value `!=` the stored value, it calls `setState` with the new value; otherwise nothing happens. 4. If the widget is rebuilt with a different `selector` function, it re-selects from the current state — a fix shipped in flutter_bloc 9.1.1. The comparison uses `T`'s `==`. That single fact decides whether `BlocSelector` helps or silently rebuilds on every state. ## Choosing values that compare well | Selected value | Equality | Behaviour | |---|---|---| | `bool`, `int`, `String`, enum | value | rebuilds only on change | | record such as `(valid: true, submitting: false)` | field by field | rebuilds when any field changes | | `Equatable` object | props | rebuilds when props change | | new plain object or `List` literal | identity | rebuilds on **every** state | | mutated shared list | same instance | **never** rebuilds | The docs' requirement that the selected value be immutable covers the last row: a list mutated in place is equal to itself, so the change is invisible. ## BlocSelector versus buildWhen | | `BlocBuilder` + `buildWhen` | `BlocSelector` | |---|---|---| | Builder receives | whole state | selected value | | Filter | hand-written `(previous, current)` | automatic `!=` on the selected value | | Stale-field risk | yes, if the filter misses a field | no, builder only sees what is compared | | Best for | several fields, whole state | one value or a small record | The structural advantage: with a selector the builder physically cannot use a field that the filter ignores, so the filter and the rendering never drift apart. ## The login submit button A submit button needs two things: whether the form is valid, and whether a submission is running. A record carries both: ```dart BlocSelector<LoginCubit, LoginState, ({bool valid, bool submitting})>( selector: (state) => ( valid: state.isValid, submitting: state.status == LoginStatus.submitting, ), builder: (context, s) => FilledButton( onPressed: s.valid && !s.submitting ? () => context.read<LoginCubit>().submit() : null, child: s.submitting ? const CircularProgressIndicator() : const Text('Sign in'), ), ) ``` Typing into the email field emits new states, but the button rebuilds only when validity or the submitting flag actually flips. ## Pitfalls - Selecting the whole state (`(s) => s`) turns it back into a plain `BlocBuilder`. - Returning a `List` literal such as `[s.isValid, s.status]` rebuilds every time, because two lists compare by identity. - Doing expensive work in `selector`: it runs for every state, so keep it a cheap projection. - Needing a side effect: `BlocSelector` has no listener; pair it with a `BlocListener`.

  • Why does a BlocSelector that returns a List literal rebuild on every state?
    BlocSelector compares the new and old selected values with `!=`, and Dart's `List` uses identity equality. Each call to the selector builds a new list, so it is never equal to the previous one. Return a record, an Equatable object or a primitive instead.
  • Can BlocSelector react to a state it filters out, for example to show a snackbar?
    No. It only builds; there is no listener callback. Keep the selector for rendering and add a BlocListener with a listenWhen for the one-off reaction.

saying these in an interview costs you the question

  • BlocSelector's builder receives the whole state
  • BlocSelector accepts a buildWhen to refine its filtering
  • Returning a new List from selector rebuilds only when elements change
  • BlocSelector compares selected values by hashCode
  • A mutated list in the selection still triggers a rebuild