flutter
/

BlocConsumer: Combining UI Rebuilds and Side Effects

Last Sync: Today

On this page

11
0%
Intermediate
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutterIntermediate

BlocConsumer: Combining UI Rebuilds and Side Effects

When building reactive UIs with Bloc, you often need to both rebuild the UI in response to state changes and perform side effects like navigation or showing snackbars. While you can use separate BlocBuilder and BlocListener widgets, BlocConsumer combines both in one convenient widget. This guide covers everything you need to know about BlocConsumer – when to use it, how to configure it, and best practices.

What is BlocConsumer?

BlocConsumer is a widget that combines BlocBuilder and BlocListener. It accepts both a builder (to rebuild the UI) and a listener (to handle side effects). It's particularly useful when you need both capabilities for the same bloc and want to keep the code concise.

Basic Usage

To use BlocConsumer, provide a listener function for side effects and a builder function for UI. The listener runs when the state changes (after the builder), and the builder rebuilds the UI.

DARTRead-only
1
BlocConsumer<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)),
      );
    }
  },
  builder: (context, state) {
    if (state is AuthLoading) {
      return const Center(child: CircularProgressIndicator());
    }
    if (state is AuthFailure) {
      return Text(state.message);
    }
    return LoginForm();
  },
);

Fine‑tuning with listenWhen and buildWhen

Like BlocBuilder and BlocListener, BlocConsumer supports listenWhen and buildWhen to control exactly when each part runs.

DARTRead-only
1
BlocConsumer<AuthBloc, AuthState>(
  listenWhen: (previous, current) => current is AuthSuccess || current is AuthFailure,
  listener: (context, state) {
    // only handle success/failure, ignore other states
  },
  buildWhen: (previous, current) => current is! AuthSuccess && current is! AuthFailure,
  builder: (context, state) {
    // rebuild only for non-terminal states
  },
);

When to Use BlocConsumer vs Separate Builder and Listener

ScenarioRecommendation
Both builder and listener are needed for the same blocUse BlocConsumer
Only builder is neededUse BlocBuilder
Only listener is neededUse BlocListener
Builder and listener logic is complex and you want to separate concernsUse separate widgets for clarity
You need to place builder and listener at different positions in the widget treeUse separate widgets

In general, BlocConsumer is ideal when you need both functionalities and they are closely related. For complex UIs where the builder and listener are large or located far apart, separate widgets may be more readable.

Performance Considerations

  • Use listenWhen and buildWhen – Prevent unnecessary listener calls and rebuilds.
  • Place BlocConsumer as low as possible – Only wrap the part of the UI that actually needs the bloc to keep rebuilds localized.
  • Avoid heavy computations inside builder – If you need to compute derived data, use Selector or buildWhen.
  • For very granular rebuilds, consider using BlocSelector inside the builder – This can further reduce rebuilds.

BlocConsumer vs BlocConsumer.value

BlocConsumer also comes with a .value named constructor, similar to BlocProvider.value. It allows you to pass an existing bloc instance instead of creating a new one. This is useful when the bloc is already created elsewhere (e.g., from a parent widget) and you want to reuse it.

DARTRead-only
1
BlocConsumer<AuthBloc, AuthState>.value(
  value: existingBloc, // an existing instance
  listener: (context, state) { ... },
  builder: (context, state) { ... },
);

Complete Example

DARTRead-only
1
class LoginPage extends StatelessWidget {
  final TextEditingController emailController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: BlocConsumer<AuthBloc, AuthState>(
        listenWhen: (previous, current) => current is AuthSuccess || current is AuthFailure,
        listener: (context, state) {
          if (state is AuthSuccess) {
            Navigator.pushReplacementNamed(context, '/home');
          } else 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 Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                TextField(
                  controller: emailController,
                  decoration: const InputDecoration(labelText: 'Email'),
                ),
                TextField(
                  controller: passwordController,
                  decoration: const InputDecoration(labelText: 'Password'),
                  obscureText: true,
                ),
                const SizedBox(height: 20),
                ElevatedButton(
                  onPressed: () {
                    context.read<AuthBloc>().add(
                      LoginRequested(
                        emailController.text,
                        passwordController.text,
                      ),
                    );
                  },
                  child: const Text('Login'),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

Best Practices

  • Use BlocConsumer when you need both builder and listener – Avoid unnecessary nesting of separate widgets.
  • Always provide a child when using BlocConsumer – The builder returns a widget; the listener does not require a child, but the widget itself must return a widget tree.
  • Use listenWhen to filter side effects – Prevents unnecessary navigation or snackbar calls.
  • Use buildWhen to filter UI rebuilds – Improves performance.
  • Place BlocConsumer as close to the UI that depends on the state as possible – Keeps rebuilds local.
  • Separate concerns – If the builder and listener are very large, consider splitting into smaller widgets for readability.

Common Mistakes

  • ❌ Using BlocConsumer when only builder or only listener is needed – Adds unnecessary complexity. ✅ Use BlocBuilder or BlocListener individually.
  • ❌ Forgetting to handle all states in builder – UI may break. ✅ Always define a fallback or handle all possible states.
  • ❌ Calling emit inside listener – This would cause an infinite loop. ✅ Use listener for side effects, not for state changes.
  • ❌ Placing BlocConsumer too high in the tree – Rebuilds more than necessary. ✅ Place it as low as possible.
  • ❌ Not using listenWhen or buildWhen when state changes frequently – Performance degradation. ✅ Use filters to limit rebuilds and listener calls.

Next Steps

Now that you've mastered BlocConsumer, explore other Bloc topics:

  • BlocProvider – Providing blocs to the widget tree.
  • BlocBuilder & BlocListener – In‑depth look at each.
  • Bloc Performance Optimization – Advanced tips.

Conclusion

BlocConsumer is a versatile widget that combines the power of BlocBuilder and BlocListener. It simplifies your code when you need both UI rebuilds and side effects for the same bloc. By using listenWhen and buildWhen, you can keep your app performant and your UI responsive. Use it wisely to build clean, maintainable Flutter applications.

Try it yourself

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

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

// States
abstract class CounterState extends Equatable {}
class CounterInitial extends CounterState { @override List<Object?> get props => []; }
class CounterLoading extends CounterState { @override List<Object?> get props => []; }
class CounterValue extends CounterState {
  final int value;
  CounterValue(this.value);
  @override List<Object?> get props => [value];
}
class CounterError extends CounterState {
  final String message;
  CounterError(this.message);
  @override List<Object?> get props => [message];
}

// Events
abstract class CounterEvent {}
class FetchCounter extends CounterEvent {}

// Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterInitial()) {
    on<FetchCounter>((event, emit) async {
      emit(CounterLoading());
      await Future.delayed(const Duration(seconds: 1));
      emit(CounterValue(42));
      // simulate error
      // emit(CounterError('Something went wrong!'));
    });
  }
}

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('BlocConsumer Demo')),
      body: BlocConsumer<CounterBloc, CounterState>(
        listenWhen: (previous, current) => current is CounterError,
        listener: (context, state) {
          if (state is CounterError) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(state.message)),
            );
          }
        },
        builder: (context, state) {
          if (state is CounterLoading) {
            return const Center(child: CircularProgressIndicator());
          }
          if (state is CounterValue) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Value: ${state.value}', style: const TextStyle(fontSize: 32)),
                  const SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: () => context.read<CounterBloc>().add(FetchCounter()),
                    child: const Text('Refresh'),
                  ),
                ],
              ),
            );
          }
          return Center(
            child: ElevatedButton(
              onPressed: () => context.read<CounterBloc>().add(FetchCounter()),
              child: const Text('Fetch Data'),
            ),
          );
        },
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

What are the two required parameters of BlocConsumer?

A
builder and listener
B
builder and child
C
listener and child
D
builder and bloc
Q2
of 3

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

A
When you need both UI rebuilds and side effects from the same bloc
B
When you only need UI rebuilds
C
When you only need side effects
D
Always
Q3
of 3

What parameter allows you to control when the listener runs?

A
buildWhen
B
listenWhen
C
selector
D
condition

Frequently Asked Questions

Can I use BlocConsumer without providing a bloc?

No, BlocConsumer expects a bloc to be available in the context (via BlocProvider) or you must pass it via the .value constructor.

What is the execution order of builder and listener?

The listener runs after the builder. This ensures that the UI is updated first, then side effects occur.

Can I have multiple BlocConsumers for the same bloc?

Yes, you can. Each will have its own builder and listener. This is useful for splitting UI into independent parts.

How do I handle initial state with BlocConsumer?

The builder is called immediately with the initial state. The listener will not be called on the initial state unless it matches the listenWhen condition.

Should I use BlocConsumer or separate BlocBuilder + BlocListener?

It depends. If the builder and listener are closely related and placed together, BlocConsumer is convenient. If you need to place them at different positions in the tree or one of them is optional, use separate widgets.

Previous

bloc listener

Next

bloc selector

Related Content

Need help?

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