Flutter BLoC Tutorial: MultiBlocProvider with Advanced Cart Example
🧠 Introduction: Why Use MultiBlocProvider?
As your Flutter app grows, you’ll often need to manage multiple types of state: user authentication, app theme, shopping cart, etc. Instead of crowding all logic into one BLoC or Cubit, Flutter’s MultiBlocProvider
helps you cleanly inject multiple BLoCs or Cubits into the widget tree.
This article expands our previous cart example by integrating MultiBlocProvider
and implementing:
- A CartBloc with full add, remove, update, clear functionality
- A ThemeCubit to toggle light and dark themes
- An AuthBloc to simulate login/logout
All components work together to demonstrate how you can scale state management in real apps.
📅 Project Structure
lib/
├── blocs/
│ ├── auth_bloc.dart
│ ├── cart_bloc.dart
│ ├── cart_event.dart
│ ├── cart_state.dart
│ ├── theme_cubit.dart
├── models/
│ └── cart_item.dart
├── screens/
│ └── cart_screen.dart
└── main.dart
🔹 Step 1: Cart Item Model
// lib/models/cart_item.dart
class CartItem {
final String id;
final String name;
final int quantity;
CartItem({
required this.id,
required this.name,
required this.quantity,
});
CartItem copyWith({String? name, int? quantity}) {
return CartItem(
id: id,
name: name ?? this.name,
quantity: quantity ?? this.quantity,
);
}
}
🔹 Step 2: Cart Events
// lib/blocs/cart_event.dart
import '../models/cart_item.dart';
abstract class CartEvent {}
class AddItem extends CartEvent {
final CartItem item;
AddItem(this.item);
}
class RemoveItem extends CartEvent {
final String itemId;
RemoveItem(this.itemId);
}
class UpdateItemQuantity extends CartEvent {
final String itemId;
final int quantity;
UpdateItemQuantity(this.itemId, this.quantity);
}
class ClearCart extends CartEvent {}
class LoadCart extends CartEvent {}
🔹 Step 3: Cart States
// lib/blocs/cart_state.dart
import '../models/cart_item.dart';
abstract class CartState {}
class CartInitial extends CartState {}
class CartLoading extends CartState {}
class CartLoaded extends CartState {
final List<CartItem> items;
CartLoaded(this.items);
}
class CartError extends CartState {
final String message;
CartError(this.message);
}
🔹 Step 4: CartBloc
// lib/blocs/cart_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/cart_item.dart';
import 'cart_event.dart';
import 'cart_state.dart';
class CartBloc extends Bloc<CartEvent, CartState> {
List<CartItem> _cartItems = [];
CartBloc() : super(CartInitial()) {
on<LoadCart>((event, emit) {
emit(CartLoaded(_cartItems));
});
on<AddItem>((event, emit) {
_cartItems.add(event.item);
emit(CartLoaded(List.from(_cartItems)));
});
on<RemoveItem>((event, emit) {
_cartItems.removeWhere((item) => item.id == event.itemId);
emit(CartLoaded(List.from(_cartItems)));
});
on<UpdateItemQuantity>((event, emit) {
_cartItems = _cartItems.map((item) {
if (item.id == event.itemId) {
return item.copyWith(quantity: event.quantity);
}
return item;
}).toList();
emit(CartLoaded(List.from(_cartItems)));
});
on<ClearCart>((event, emit) {
_cartItems.clear();
emit(CartLoaded([]));
});
}
}
🔹 Step 5: ThemeCubit
// lib/blocs/theme_cubit.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ThemeCubit extends Cubit<ThemeMode> {
ThemeCubit() : super(ThemeMode.light);
void toggleTheme() {
emit(state == ThemeMode.light ? ThemeMode.dark : ThemeMode.light);
}
}
🔹 Step 6: AuthBloc
// lib/blocs/auth_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class AuthEvent {}
class LoginRequested extends AuthEvent {}
class LogoutRequested extends AuthEvent {}
class AuthBloc extends Bloc<AuthEvent, bool> {
AuthBloc() : super(false) {
on<LoginRequested>((event, emit) => emit(true));
on<LogoutRequested>((event, emit) => emit(false));
}
}
🔹 Step 7: Main with MultiBlocProvider
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'blocs/auth_bloc.dart';
import 'blocs/cart_bloc.dart';
import 'blocs/theme_cubit.dart';
import 'screens/cart_screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<CartBloc>(create: (_) => CartBloc()..add(LoadCart())),
BlocProvider<ThemeCubit>(create: (_) => ThemeCubit()),
BlocProvider<AuthBloc>(create: (_) => AuthBloc()),
],
child: BlocBuilder<ThemeCubit, ThemeMode>(
builder: (context, mode) {
return MaterialApp(
title: 'MultiBlocProvider Example',
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: mode,
home: CartScreen(),
);
},
),
);
}
}
🔹 Step 8: CartScreen with BLoC Logic
// lib/screens/cart_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/cart_bloc.dart';
import '../blocs/cart_event.dart';
import '../blocs/cart_state.dart';
import '../blocs/theme_cubit.dart';
import '../blocs/auth_bloc.dart';
import '../models/cart_item.dart';
class CartScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cartBloc = context.read<CartBloc>();
final isDark = context.watch<ThemeCubit>().state == ThemeMode.dark;
final isLoggedIn = context.watch<AuthBloc>().state;
return Scaffold(
appBar: AppBar(
title: BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
final count = (state is CartLoaded) ? state.items.length : 0;
return Text('Cart ($count)');
},
),
actions: [
IconButton(
icon: Icon(isDark ? Icons.dark_mode : Icons.light_mode),
onPressed: () => context.read<ThemeCubit>().toggleTheme(),
),
IconButton(
icon: Icon(isLoggedIn ? Icons.logout : Icons.login),
onPressed: () => context.read<AuthBloc>().add(
isLoggedIn ? LogoutRequested() : LoginRequested(),
),
)
],
),
body: BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
if (state is CartLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is CartLoaded) {
return ListView.builder(
itemCount: state.items.length,
itemBuilder: (context, index) {
final item = state.items[index];
return ListTile(
title: Text(item.name),
subtitle: Text('Qty: ${item.quantity}'),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => cartBloc.add(RemoveItem(item.id)),
),
onTap: () => cartBloc.add(UpdateItemQuantity(
item.id,
item.quantity + 1,
)),
);
},
);
} else {
return Center(child: Text('No items in cart'));
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
final item = CartItem(
id: DateTime.now().toString(),
name: 'Item ${DateTime.now().second}',
quantity: 1,
);
cartBloc.add(AddItem(item));
},
child: Icon(Icons.add),
),
);
}
}
✅ Summary
Concept | Description |
---|---|
MultiBlocProvider |
Injects multiple BLoCs/Cubits at once |
CartBloc |
Manages cart items: add, remove, update, clear |
ThemeCubit |
Toggles app theme mode |
AuthBloc |
Simulates login/logout |
BlocBuilder |
Rebuilds UI when state changes |
context.read() |
Access bloc/cubit without listening |
context.watch() |
Access bloc/cubit and rebuilds when state updates |
📘 Todo App with Full CRUD & Filtering
In this tutorial, we’ll build a complete Todo App using Flutter BLoC. This app will feature:
- Add / Edit / Delete todos
- Mark todos as complete/incomplete
- Filter views (All, Completed, Active)
- State management using
Bloc
, not Cubit
This guide is perfect for anyone who has understood basic BLoC and wants to explore event-state workflows in a real-world CRUD app.
📅 Project Structure
lib/
├── blocs/
│ ├── todo_bloc.dart
│ ├── todo_event.dart
│ ├── todo_state.dart
├── models/
│ └── todo.dart
├── screens/
│ └── todo_screen.dart
└── main.dart
🔹 Step 1: Todo Model
// lib/models/todo.dart
class Todo {
final String id;
final String title;
final bool isCompleted;
Todo({
required this.id,
required this.title,
this.isCompleted = false,
});
Todo copyWith({String? title, bool? isCompleted}) {
return Todo(
id: id,
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
);
}
}
🔹 Step 2: Events
// lib/blocs/todo_event.dart
import '../models/todo.dart';
abstract class TodoEvent {}
class LoadTodos extends TodoEvent {}
class AddTodo extends TodoEvent {
final Todo todo;
AddTodo(this.todo);
}
class UpdateTodo extends TodoEvent {
final String id;
final String newTitle;
UpdateTodo(this.id, this.newTitle);
}
class ToggleTodo extends TodoEvent {
final String id;
ToggleTodo(this.id);
}
class DeleteTodo extends TodoEvent {
final String id;
DeleteTodo(this.id);
}
class FilterTodos extends TodoEvent {
final String filter; // all, completed, active
FilterTodos(this.filter);
}
🔹 Step 3: States
// lib/blocs/todo_state.dart
import '../models/todo.dart';
abstract class TodoState {}
class TodoInitial extends TodoState {}
class TodoLoading extends TodoState {}
class TodoLoaded extends TodoState {
final List<Todo> todos;
final String filter;
TodoLoaded(this.todos, {this.filter = "all"});
}
class TodoError extends TodoState {
final String message;
TodoError(this.message);
}
🔹 Step 4: TodoBloc
// lib/blocs/todo_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'todo_event.dart';
import 'todo_state.dart';
import '../models/todo.dart';
class TodoBloc extends Bloc<TodoEvent, TodoState> {
List<Todo> _todos = [];
String _filter = "all";
TodoBloc() : super(TodoInitial()) {
on<LoadTodos>((event, emit) {
emit(TodoLoaded(List.from(_todos), filter: _filter));
});
on<AddTodo>((event, emit) {
_todos.add(event.todo);
emit(TodoLoaded(List.from(_filteredTodos()), filter: _filter));
});
on<UpdateTodo>((event, emit) {
_todos = _todos.map((t) =>
t.id == event.id ? t.copyWith(title: event.newTitle) : t).toList();
emit(TodoLoaded(List.from(_filteredTodos()), filter: _filter));
});
on<ToggleTodo>((event, emit) {
_todos = _todos.map((t) =>
t.id == event.id ? t.copyWith(isCompleted: !t.isCompleted) : t).toList();
emit(TodoLoaded(List.from(_filteredTodos()), filter: _filter));
});
on<DeleteTodo>((event, emit) {
_todos.removeWhere((t) => t.id == event.id);
emit(TodoLoaded(List.from(_filteredTodos()), filter: _filter));
});
on<FilterTodos>((event, emit) {
_filter = event.filter;
emit(TodoLoaded(List.from(_filteredTodos()), filter: _filter));
});
}
List<Todo> _filteredTodos() {
if (_filter == "completed") {
return _todos.where((t) => t.isCompleted).toList();
} else if (_filter == "active") {
return _todos.where((t) => !t.isCompleted).toList();
}
return _todos;
}
}
🔹 Step 5: UI – todo_screen.dart
// lib/screens/todo_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/todo_bloc.dart';
import '../blocs/todo_event.dart';
import '../blocs/todo_state.dart';
import '../models/todo.dart';
class TodoScreen extends StatelessWidget {
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
final todoBloc = context.read<TodoBloc>();
return Scaffold(
appBar: AppBar(
title: Text('Todo BLoC App'),
actions: [
PopupMenuButton<String>(
onSelected: (value) {
todoBloc.add(FilterTodos(value));
},
itemBuilder: (_) => [
PopupMenuItem(value: "all", child: Text("All")),
PopupMenuItem(value: "active", child: Text("Active")),
PopupMenuItem(value: "completed", child: Text("Completed")),
],
)
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(hintText: "Add todo"),
),
),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
final todo = Todo(
id: DateTime.now().toString(),
title: _controller.text,
);
todoBloc.add(AddTodo(todo));
_controller.clear();
},
)
],
),
),
Expanded(
child: BlocBuilder<TodoBloc, TodoState>(
builder: (context, state) {
if (state is TodoLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is TodoLoaded) {
return ListView.builder(
itemCount: state.todos.length,
itemBuilder: (context, index) {
final todo = state.todos[index];
return ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) =>
todoBloc.add(ToggleTodo(todo.id)),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => todoBloc.add(DeleteTodo(todo.id)),
),
);
},
);
}
return Center(child: Text("No todos"));
},
),
)
],
),
);
}
}
🔹 Step 6: main.dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'blocs/todo_bloc.dart';
import 'blocs/todo_event.dart';
import 'screens/todo_screen.dart';
void main() {
runApp(
BlocProvider(
create: (_) => TodoBloc()..add(LoadTodos()),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo BLoC CRUD',
home: TodoScreen(),
);
}
}
✅ Summary
Feature | Description |
---|---|
TodoBloc | Manages todo logic with events and states |
BlocProvider | Injects the BLoC into widget tree |
BlocBuilder | Rebuilds UI on state changes |
CRUD Operations | Add, update, toggle, delete todo items |
Filtering | View All, Completed, Active todos |
In the next article, we’ll connect the Todo app to local storage or a database for persistence.