flutter
/

BLoC buildWhen & listenWhen: Fine-Tuning UI Updates & Side Effects

Last Sync: Today

On this page

9
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

BLoC buildWhen & listenWhen: Fine-Tuning UI Updates & Side Effects

Introduction

In BLoC, BlocBuilder and BlocListener provide powerful ways to react to state changes. By default, BlocBuilder rebuilds its child on every state emission, and BlocListener executes its listener on every emission. This can lead to unnecessary rebuilds or side effects. The buildWhen and listenWhen parameters give you fine‑grained control to filter which state changes trigger rebuilds or side effects, significantly improving performance and user experience.

Understanding buildWhen

buildWhen is a predicate function that determines whether BlocBuilder should rebuild its child. It receives the previous state and the current state and returns a bool. If true, the builder runs; if false, it skips the rebuild.

DARTRead-only
1
BlocBuilder<CounterBloc, CounterState>(
  buildWhen: (previous, current) {
    // Only rebuild when the count changes, not when isLoading changes
    return previous.count != current.count;
  },
  builder: (context, state) {
    return Text('Count: ${state.count}');
  },
);

Understanding listenWhen

listenWhen works similarly for BlocListener. It decides whether the listener callback should be invoked. This is useful for side effects like showing a snackbar, navigating, or logging.

DARTRead-only
1
BlocListener<LoginBloc, LoginState>(
  listenWhen: (previous, current) {
    // Show snackbar only on error, not on other states
    return current is LoginError && previous is! LoginError;
  },
  listener: (context, state) {
    if (state is LoginError) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.message)),
      );
    }
  },
  child: ...,
);

Real‑World Example: User Profile Screen

Consider a profile screen that displays user details and also shows a loading indicator. You might want to rebuild the name widget only when the name changes, but show a snackbar only on error.

DARTRead-only
1
class ProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => ProfileBloc()..add(LoadProfile()),
      child: Scaffold(
        appBar: AppBar(title: Text('Profile')),
        body: Column(
          children: [
            // Rebuild only when the name changes
            BlocBuilder<ProfileBloc, ProfileState>(
              buildWhen: (previous, current) => previous.user.name != current.user.name,
              builder: (context, state) => Text('Name: ${state.user.name}'),
            ),
            // Rebuild only when loading changes (for loading indicator)
            BlocBuilder<ProfileBloc, ProfileState>(
              buildWhen: (previous, current) => previous.isLoading != current.isLoading,
              builder: (context, state) => state.isLoading
                  ? CircularProgressIndicator()
                  : Container(),
            ),
          ],
        ),
      ),
    );
  }
}

Using BlocConsumer for Combined Control

BlocConsumer combines BlocBuilder and BlocListener in a single widget. You can provide separate buildWhen and listenWhen to independently control rebuilds and side effects.

DARTRead-only
1
BlocConsumer<LoginBloc, LoginState>(
  listenWhen: (previous, current) {
    // Side effect only on transition to error or success
    return (current is LoginError && previous is! LoginError) ||
           (current is LoginSuccess && previous is! LoginSuccess);
  },
  listener: (context, state) {
    if (state is LoginError) {
      showError(state.message);
    } else if (state is LoginSuccess) {
      navigateToHome();
    }
  },
  buildWhen: (previous, current) {
    // Rebuild UI only when loading status changes
    return previous.isLoading != current.isLoading;
  },
  builder: (context, state) {
    return state.isLoading ? CircularProgressIndicator() : LoginForm();
  },
);

Advanced Patterns

For even finer control, you can combine buildWhen with context.select (which listens to a single property). Both approaches achieve similar results; choose based on readability.

DARTRead-only
1
// Using buildWhen
BlocBuilder<CartBloc, CartState>(
  buildWhen: (prev, curr) => prev.total != curr.total,
  builder: (context, state) => Text('Total: ${state.total}'),
);

// Using context.select (alternative)
final total = context.select((CartBloc bloc) => bloc.state.total);
Text('Total: $total');

You can also implement debouncing inside listenWhen to prevent repeated side effects (e.g., showing the same error multiple times). However, it's simpler to use listenWhen to filter duplicates.

DARTRead-only
1
BlocListener<NotificationBloc, NotificationState>(
  listenWhen: (previous, current) {
    // Only trigger when the error message is new
    if (current is NotificationError) {
      return previous is! NotificationError || previous.message != current.message;
    }
    return false;
  },
  listener: (context, state) {
    if (state is NotificationError) {
      showSnackbar(state.message);
    }
  },
  child: ...,
);

Best Practices

  • Use buildWhen to prevent unnecessary rebuilds – Especially for widgets that depend only on part of the state.
  • Use listenWhen to avoid duplicate side effects – Show snackbars only once per error, navigate only on transition, etc.
  • Keep predicates simple – Complex logic inside buildWhen can be hard to maintain; extract to a method if needed.
  • Combine with Equatable – Makes state comparison easier and reduces errors.
  • Consider context.select for simple cases – It can be cleaner than a separate buildWhen.
  • Test your filters – Ensure buildWhen and listenWhen return the correct boolean in unit tests.

Common Mistakes

  • ❌ Using buildWhen that always returns true – No benefit, adds clutter. ✅ Use only when necessary.
  • ❌ Forgetting to handle initial state in listenWhen – May cause side effects on first state. ✅ Check previous for initial.
  • ❌ Over‑filtering – Miss important updates. ✅ Test thoroughly to ensure correct behavior.
  • ❌ Mutating state inside buildWhen – Should be pure; no side effects. ✅ Keep predicate read‑only.

Conclusion

buildWhen and listenWhen are essential tools for optimizing BLoC‑powered Flutter apps. By selectively controlling rebuilds and side effects, you improve performance, reduce noise, and create a smoother user experience. Always consider which part of the state actually matters for each UI component and side effect.

Try it yourself

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';

void main() => runApp(MyApp());

// State
class CounterState extends Equatable {
  final int count;
  final bool isLoading;

  const CounterState({required this.count, this.isLoading = false});

  CounterState copyWith({int? count, bool? isLoading}) {
    return CounterState(
      count: count ?? this.count,
      isLoading: isLoading ?? this.isLoading,
    );
  }

  @override
  List<Object?> get props => [count, isLoading];
}

// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class StartLoading extends CounterEvent {}
class StopLoading extends CounterEvent {}

// BLoC
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState(count: 0)) {
    on<Increment>((event, emit) {
      emit(state.copyWith(count: state.count + 1));
    });
    on<StartLoading>((event, emit) {
      emit(state.copyWith(isLoading: true));
    });
    on<StopLoading>((event, emit) {
      emit(state.copyWith(isLoading: false));
    });
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('buildWhen & listenWhen Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Rebuilds only when count changes (not on isLoading)
            BlocBuilder<CounterBloc, CounterState>(
              buildWhen: (previous, current) => previous.count != current.count,
              builder: (context, state) => Text(
                'Count: ${state.count}',
                style: TextStyle(fontSize: 40),
              ),
            ),
            const SizedBox(height: 20),
            // Rebuilds only when isLoading changes
            BlocBuilder<CounterBloc, CounterState>(
              buildWhen: (previous, current) => previous.isLoading != current.isLoading,
              builder: (context, state) => state.isLoading
                  ? const CircularProgressIndicator()
                  : const Text('Not loading'),
            ),
            const SizedBox(height: 20),
            // Listener only shows snackbar on error (no error in this demo, but pattern shown)
            BlocListener<CounterBloc, CounterState>(
              listenWhen: (previous, current) {
                // This demo doesn't have error state, but you'd listen to error transitions
                return false;
              },
              listener: (context, state) {},
              child: const SizedBox(),
            ),
            ElevatedButton(
              onPressed: () => context.read<CounterBloc>().add(Increment()),
              child: const Text('Increment'),
            ),
            ElevatedButton(
              onPressed: () => context.read<CounterBloc>().add(StartLoading()),
              child: const Text('Start Loading'),
            ),
            ElevatedButton(
              onPressed: () => context.read<CounterBloc>().add(StopLoading()),
              child: const Text('Stop Loading'),
            ),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

What does `buildWhen` return to prevent a rebuild?

A
true
B
false
C
void
D
null
Q2
of 3

Which widget combines `BlocBuilder` and `BlocListener` with separate filters?

A
BlocConsumer
B
BlocProvider
C
BlocSelector
D
MultiBlocListener
Q3
of 3

What is the primary benefit of using `listenWhen`?

A
It reduces rebuilds
B
It prevents side effects from running on irrelevant state changes
C
It improves testability
D
It automatically debounces events

Frequently Asked Questions

What is the difference between `buildWhen` and `context.select`?

buildWhen is a predicate that receives previous and current state. context.select directly listens to a specific property. Both can achieve similar results; context.select is often less code for simple cases.

Can I use `buildWhen` with `BlocConsumer`?

Yes, BlocConsumer accepts both buildWhen and listenWhen separately.

How do I test `buildWhen`?

You can test it by verifying that the builder is called only when expected. However, since buildWhen is internal to the widget, you usually test by ensuring the UI updates correctly under different state sequences.

Does `buildWhen` affect performance?

Yes, positively. It reduces rebuilds, which improves performance. The predicate itself is very cheap.

Can I use `buildWhen` to ignore the first emission?

Yes, check previous state. For example, buildWhen: (prev, curr) => prev != curr but with initial state handling.

Previous

bloc initial state

Next

bloc split widgets

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.