Lesson 34 of 51 intermediate

flutter_bloc in Practice

Wiring the BLoC brain to the Flutter body

Open interactive version (quiz + challenge)

Real-world analogy

You built a powerful engine (the BLoC) in the last lesson. Now you need to put it in a car and connect everything. BlocProvider is the engine mount that holds the BLoC in the widget tree. BlocBuilder is the dashboard that displays engine data. BlocListener is the alarm system that beeps when something important happens. BlocConsumer is a dashboard with a built-in alarm. And MultiBlocProvider is a garage that can hold many engines at once.

What is it?

flutter_bloc is the Flutter package that connects BLoC business logic to the widget tree. BlocProvider creates and scopes a BLoC instance to a widget subtree. BlocBuilder rebuilds UI when state changes. BlocListener triggers side effects like navigation or showing dialogs. BlocConsumer combines both building and listening. MultiBlocProvider and MultiBlocListener provide clean syntax for multiple BLoCs.

Real-world relevance

In team_mvp_kit, every feature screen is wrapped with a BlocProvider that creates the feature's BLoC. The screen body uses BlocBuilder for the main content, BlocListener for navigation and error toasts, and sometimes BlocConsumer when both are needed. MultiBlocProvider at the app level provides global BLoCs like auth and theme.

Key points

Code example

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

// Assume TodoBloc, TodoState, BaseEvent from Lesson 33

class TodoApp extends StatelessWidget {
  const TodoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider(
          create: (context) => TodoBloc()..add(LoadTodos()),
        ),
      ],
      child: MaterialApp(
        title: 'BLoC Todo',
        home: const TodoScreen(),
      ),
    );
  }
}

class TodoScreen extends StatelessWidget {
  const TodoScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Todos'),
        actions: [
          BlocBuilder<TodoBloc, TodoState>(
            buildWhen: (prev, curr) =>
              prev.todos.length != curr.todos.length,
            builder: (context, state) {
              final done = state.todos
                  .where((t) => t.isDone).length;
              return Center(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Text('$done/${state.todos.length}'),
                ),
              );
            },
          ),
        ],
      ),
      body: BlocConsumer<TodoBloc, TodoState>(
        listenWhen: (prev, curr) =>
          prev.error != curr.error && curr.error != null,
        listener: (context, state) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text(state.error!),
              backgroundColor: Colors.red,
            ),
          );
        },
        buildWhen: (prev, curr) =>
          prev.todos != curr.todos ||
          prev.isLoading != curr.isLoading,
        builder: (context, state) {
          if (state.isLoading) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }
          if (state.todos.isEmpty) {
            return const Center(
              child: Text('No todos yet!'),
            );
          }
          return ListView.builder(
            itemCount: state.todos.length,
            itemBuilder: (context, index) {
              final todo = state.todos[index];
              return Dismissible(
                key: ValueKey('$index-${todo.title}'),
                onDismissed: (_) {
                  context.read<TodoBloc>()
                    .add(DeleteTodo(index));
                },
                child: CheckboxListTile(
                  title: Text(
                    todo.title,
                    style: TextStyle(
                      decoration: todo.isDone
                          ? TextDecoration.lineThrough
                          : null,
                    ),
                  ),
                  value: todo.isDone,
                  onChanged: (_) {
                    context.read<TodoBloc>()
                      .add(ToggleTodo(index));
                  },
                ),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddDialog(context),
        child: const Icon(Icons.add),
      ),
    );
  }

  void _showAddDialog(BuildContext context) {
    final controller = TextEditingController();
    showDialog(
      context: context,
      builder: (dialogContext) => AlertDialog(
        title: const Text('Add Todo'),
        content: TextField(
          controller: controller,
          autofocus: true,
          decoration: const InputDecoration(
            hintText: 'Enter todo title',
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(dialogContext),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () {
              if (controller.text.isNotEmpty) {
                context.read<TodoBloc>()
                  .add(AddTodo(controller.text));
                Navigator.pop(dialogContext);
              }
            },
            child: const Text('Add'),
          ),
        ],
      ),
    );
  }
}

Line-by-line walkthrough

  1. 1. TodoApp is the root widget wrapping everything in MultiBlocProvider to make BLoCs available.
  2. 2. BlocProvider creates the TodoBloc and immediately adds a LoadTodos event using the cascade operator.
  3. 3. MaterialApp and TodoScreen are children that can now access the TodoBloc.
  4. 4. TodoScreen is a StatelessWidget because all state lives in the BLoC, not in the widget.
  5. 5. The AppBar contains a BlocBuilder with buildWhen that only triggers when the todo count changes.
  6. 6. Inside the AppBar builder, we count completed todos and display a progress fraction.
  7. 7. The body uses BlocConsumer to combine building the list AND listening for errors.
  8. 8. listenWhen filters so the listener only fires when a new error appears.
  9. 9. The listener shows a red SnackBar with the error message as a side effect.
  10. 10. buildWhen triggers rebuilds when todos or loading status change.
  11. 11. The builder checks isLoading first to show a spinner, then checks for empty list.
  12. 12. ListView.builder creates a scrollable list of Dismissible CheckboxListTile widgets.
  13. 13. Dismissible enables swipe-to-delete by dispatching DeleteTodo event on dismiss.
  14. 14. context.read() is used in callbacks because these are not build methods.
  15. 15. The FloatingActionButton opens a dialog for adding new todos.
  16. 16. _showAddDialog creates a TextEditingController and an AlertDialog with a TextField.
  17. 17. The Add button reads the text, dispatches an AddTodo event, and closes the dialog.

Spot the bug

class ProductScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => ProductBloc(),
      child: Scaffold(
        appBar: AppBar(
          title: BlocBuilder<ProductBloc, ProductState>(
            builder: (context, state) {
              return Text('${state.products.length} Products');
            },
          ),
        ),
        body: BlocBuilder<ProductBloc, ProductState>(
          builder: (context, state) {
            return ListView.builder(
              itemCount: state.products.length,
              itemBuilder: (ctx, i) {
                return ListTile(
                  title: Text(state.products[i].name),
                );
              },
            );
          },
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            context.read<ProductBloc>().add(LoadProducts());
          },
        ),
      ),
    );
  }
}
Need a hint?
The FloatingActionButton's onPressed tries to read the BLoC, but is it inside the BlocProvider subtree?
Show answer
The FloatingActionButton's onPressed uses the outer 'context' from the build method, which is ABOVE the BlocProvider in the widget tree. The BlocBuilders inside AppBar and body work because they get a new context from inside the Scaffold child. Fix by wrapping the Scaffold in a Builder widget, or move BlocProvider higher up.

Explain like I'm 5

Remember the restaurant from last lesson? Now we are setting up the actual restaurant building. BlocProvider is like building the kitchen and putting it behind the counter. BlocBuilder is the menu board that automatically updates when the chef says the special changed. BlocListener is the little bell that dings when your food is ready -- it does not change the menu board, it just alerts you. BlocConsumer is a magic menu board that ALSO dings. And MultiBlocProvider is like having a food court with many kitchens all in one place!

Fun fact

The flutter_bloc package is one of the most downloaded packages on pub.dev with over 3 billion total downloads. Its creator Felix Angelov maintained it as an open-source project while working full-time, and it grew so popular that it essentially became the unofficial standard for state management in enterprise Flutter apps.

Hands-on challenge

Build a complete ProductCatalog feature with two BLoCs: ProductListBloc (handles loading, filtering, and searching products) and CartBloc (handles add to cart, remove, and total calculation). Create a screen that uses MultiBlocProvider for both BLoCs, BlocBuilder for the product grid, BlocListener on CartBloc to show a SnackBar when items are added, and buildWhen to optimize rebuilds.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart