flutter
/

BlocBuilder, BlocListener & BlocConsumer: Building Reactive UIs

Last Sync: Today

On this page

10
0%
Intermediate
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutterIntermediate

BlocBuilder, BlocListener & BlocConsumer: Building Reactive UIs

Once you've defined your blocs and provided them using BlocProvider, you need to connect them to your UI. The flutter_bloc package offers three main widgets for this: BlocBuilder, BlocListener, and BlocConsumer. This guide explains each one, when to use them, and how to get the most out of them.

BlocBuilder: Rebuilding on State Changes

BlocBuilder is a widget that rebuilds whenever the bloc's state changes. It takes a builder function that receives the current state and returns a widget. It's the go‑to widget for displaying state‑dependent content.

DARTRead-only
1
BlocBuilder<CounterCubit, CounterState>(
  builder: (context, state) {
    return Text('Count: ${state.value}');
  },
);

You can omit the bloc type if it can be inferred from the context:

DARTRead-only
1
BlocBuilder<CounterCubit, CounterState>(
  builder: (context, state) => Text('Count: ${state.value}'),
);

BlocBuilder with buildWhen

Sometimes you want to rebuild only on certain state changes. The buildWhen parameter lets you control exactly when the builder should be called. It receives the previous and current state and returns a boolean. This is a powerful performance optimisation.

DARTRead-only
1
BlocBuilder<CounterCubit, CounterState>(
  buildWhen: (previous, current) => previous.value != current.value,
  builder: (context, state) => Text('Count: ${state.value}'),
);

BlocListener: Side Effects Without Rebuilding

BlocListener is used for side effects like navigation, showing snackbars, or logging. It does not rebuild the UI. It runs a listener function whenever the state changes, but the child widget remains untouched. This prevents accidental rebuilds and infinite loops.

DARTRead-only
1
BlocListener<AuthBloc, AuthState>(
  listener: (context, state) {
    if (state is AuthSuccess) {
      Navigator.pushReplacementNamed(context, '/home');
    }
    if (state is AuthFailure) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.message)),
      );
    }
  },
  child: LoginForm(),
);

Like BlocBuilder, BlocListener also supports a listenWhen parameter to trigger the listener only on specific state changes.

DARTRead-only
1
BlocListener<AuthBloc, AuthState>(
  listenWhen: (previous, current) => current is AuthSuccess || current is AuthFailure,
  listener: (context, state) {
    // handle success or failure only
  },
  child: ...
);

BlocConsumer: Combining Builder and Listener

BlocConsumer is a convenience widget that combines BlocBuilder and BlocListener in one. It takes both a builder and a listener. Use it when you need both rebuild and side effect handling for the same bloc.

DARTRead-only
1
BlocConsumer<AuthBloc, AuthState>(
  listener: (context, state) {
    if (state is AuthSuccess) {
      Navigator.pushReplacementNamed(context, '/home');
    }
  },
  builder: (context, state) {
    if (state is AuthLoading) return CircularProgressIndicator();
    if (state is AuthFailure) return Text(state.message);
    return LoginForm();
  },
);

Both listenWhen and buildWhen can be used with BlocConsumer to fine‑tune when each part triggers.

When to Use Each Widget

WidgetUse CaseRebuildsSide Effects
BlocBuilderDisplaying state-dependent UIYesNo
BlocListenerNavigation, snackbars, loggingNoYes
BlocConsumerBoth UI and side effects from same blocYesYes

Performance Tips

  • Use buildWhen to filter unnecessary rebuilds – Especially for complex UIs, this is critical.
  • Place BlocBuilder as low as possible – Rebuild only the part that actually changes, not the whole screen.
  • Use BlocListener for side effects – Avoid putting navigation code inside BlocBuilder, as it could cause multiple calls.
  • Use context.read for event dispatching – No need to use BlocBuilder just to access the bloc.
  • Consider BlocSelector for very granular rebuilds – It selects a piece of state and rebuilds only when that piece changes.

Common Mistakes

  • ❌ Placing BlocBuilder at the top of the screen – Rebuilds the entire screen on every state change, hurting performance. ✅ Place BlocBuilder as close to the UI that depends on the state as possible.
  • ❌ Calling BlocListener without a child – The child is required (except when using it as a top‑level wrapper). ✅ Always provide a child, even if it's const SizedBox.shrink().
  • ❌ Using BlocConsumer when only one of builder/listener is needed – Adds unnecessary complexity. ✅ Use separate BlocBuilder and BlocListener when only one is needed.
  • ❌ Forgetting to handle loading/error states – UI may break or show stale data. ✅ Always define and handle all possible states.

BlocSelector: Even More Granular Rebuilds

For advanced performance, BlocSelector allows you to rebuild only when a specific part of the state changes. It selects a value from the state and rebuilds only when that value changes.

DARTRead-only
1
BlocSelector<CounterCubit, CounterState, int>(
  selector: (state) => state.value,
  builder: (context, value) {
    return Text('Count: $value');
  },
);

This is even more efficient than BlocBuilder with buildWhen because it doesn't require comparing the entire state.

Putting It All Together: Example

DARTRead-only
1
class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocConsumer<AuthBloc, AuthState>(
      listenWhen: (previous, current) => current is AuthSuccess || current is AuthFailure,
      listener: (context, state) {
        if (state is AuthSuccess) {
          Navigator.pushReplacementNamed(context, '/home');
        }
        if (state is AuthFailure) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(state.message)),
          );
        }
      },
      builder: (context, state) {
        if (state is AuthLoading) {
          return const Center(child: CircularProgressIndicator());
        }
        return LoginForm();
      },
    );
  }
}

Conclusion

BlocBuilder, BlocListener, and BlocConsumer are the essential widgets for connecting blocs to your Flutter UI. By understanding their roles and using them wisely, you can build reactive, efficient, and maintainable applications. Remember to place builders low, filter rebuilds with buildWhen, and use listeners for side effects.

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 value;
  const CounterState(this.value);
  @override List<Object?> get props => [value];
}

// Cubit
class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(const CounterState(0));
  void increment() => emit(CounterState(state.value + 1));
  void decrement() => emit(CounterState(state.value - 1));
}

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

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('BlocBuilder Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // BlocBuilder only for the count text
            BlocBuilder<CounterCubit, CounterState>(
              builder: (context, state) => Text(
                'Count: ${state.value}',
                style: const TextStyle(fontSize: 32),
              ),
            ),
            const SizedBox(height: 20),
            // BlocListener for side effect (snackbar when count reaches 5)
            BlocListener<CounterCubit, CounterState>(
              listenWhen: (previous, current) => current.value == 5,
              listener: (context, state) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('You reached 5!')),
                );
              },
              child: Container(), // required child
            ),
          ],
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => context.read<CounterCubit>().decrement(),
            child: const Icon(Icons.remove),
          ),
          const SizedBox(width: 10),
          FloatingActionButton(
            onPressed: () => context.read<CounterCubit>().increment(),
            child: const Icon(Icons.add),
          ),
        ],
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which widget should you use to show a snackbar when the state changes?

A
BlocBuilder
B
BlocListener
C
BlocConsumer
D
BlocProvider
Q2
of 3

What parameter allows you to control when BlocBuilder rebuilds?

A
buildWhen
B
listenWhen
C
selector
D
condition
Q3
of 3

When should you use BlocConsumer instead of separate BlocBuilder and BlocListener?

A
When you need both UI rebuild and side effects from the same bloc
B
When you only need side effects
C
When you only need UI rebuilds
D
Never, always use separate widgets

Frequently Asked Questions

Can I use multiple BlocBuilders for the same bloc?

Yes, you can have multiple BlocBuilders listening to the same bloc. Each will rebuild independently based on its own buildWhen logic. This is useful for splitting a large UI into smaller parts that update independently.

What's the difference between BlocBuilder and BlocConsumer?

BlocBuilder is for rebuilding UI; BlocConsumer combines both UI rebuilding and side effects. Use BlocBuilder when you only need UI updates; use BlocConsumer when you need both.

Do I need to manually call `context.read` inside BlocBuilder?

No, the bloc is automatically provided to the builder function via the context. You can also access it via context.read<MyBloc>() if needed, but the builder already gives you the state.

How can I prevent BlocBuilder from rebuilding when I don't need to?

Use the buildWhen parameter. It lets you decide whether the builder should run based on the previous and current state.

Is it okay to use BlocBuilder inside a ListView item?

Yes, but be careful with performance. If you have many items, consider using BlocSelector for each item to rebuild only when its specific data changes.

Previous

bloc multiblocprovider

Next

bloc listener

Related Content

Need help?

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