flutter_bloc in Practice
Wiring the BLoC brain to the Flutter body
Open interactive version (quiz + challenge)Real-world analogy
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
- BlocProvider — BlocProvider creates a BLoC and makes it available to all descendant widgets. It automatically disposes the BLoC when the widget is removed from the tree.
- BlocBuilder — BlocBuilder rebuilds its child widget whenever the BLoC emits a new state. It is the main way to connect BLoC state to your UI.
- buildWhen for Selective Rebuilds — The buildWhen parameter lets you control when BlocBuilder rebuilds. This is a key performance optimization -- skip rebuilds when the part of state you care about has not changed.
- BlocListener — BlocListener runs side effects in response to state changes -- like showing a SnackBar, navigating, or logging. It does NOT rebuild the UI.
- listenWhen for Selective Listening — Just like buildWhen, listenWhen controls when the listener fires. Use it to filter out state transitions you do not care about.
- BlocConsumer — BlocConsumer combines BlocBuilder and BlocListener in one widget. Use it when you need both UI rebuilds AND side effects from the same BLoC.
- MultiBlocProvider — MultiBlocProvider nests multiple BlocProviders without deep indentation. Use it at the top of your app or feature to provide all necessary BLoCs.
- MultiBlocListener — MultiBlocListener is the listener equivalent -- multiple listeners in a flat structure. Useful for a screen that reacts to several BLoCs.
- context.read vs context.watch — Use context.read() to access the BLoC without subscribing to changes (for sending events). Use context.watch().state inside build methods to subscribe and rebuild when state changes.
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. TodoApp is the root widget wrapping everything in MultiBlocProvider to make BLoCs available.
- 2. BlocProvider creates the TodoBloc and immediately adds a LoadTodos event using the cascade operator.
- 3. MaterialApp and TodoScreen are children that can now access the TodoBloc.
- 4. TodoScreen is a StatelessWidget because all state lives in the BLoC, not in the widget.
- 5. The AppBar contains a BlocBuilder with buildWhen that only triggers when the todo count changes.
- 6. Inside the AppBar builder, we count completed todos and display a progress fraction.
- 7. The body uses BlocConsumer to combine building the list AND listening for errors.
- 8. listenWhen filters so the listener only fires when a new error appears.
- 9. The listener shows a red SnackBar with the error message as a side effect.
- 10. buildWhen triggers rebuilds when todos or loading status change.
- 11. The builder checks isLoading first to show a spinner, then checks for empty list.
- 12. ListView.builder creates a scrollable list of Dismissible CheckboxListTile widgets.
- 13. Dismissible enables swipe-to-delete by dispatching DeleteTodo event on dismiss.
- 14. context.read() is used in callbacks because these are not build methods.
- 15. The FloatingActionButton opens a dialog for adding new todos.
- 16. _showAddDialog creates a TextEditingController and an AlertDialog with a TextField.
- 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?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- flutter_bloc Package (pub.dev)
- BLoC Widgets Guide (bloclibrary.dev)
- BLoC Architecture (bloclibrary.dev)