AI Reading
Quick summary of this article
This tutorial explains how to manage multiple types of state in a Flutter app using MultiBlocProvider. It demonstrates a scalable approach by combining a CartBloc, ThemeCubit, and AuthBloc in one widget tree, and also covers building a Todo app with full CRUD operations and filtering using BLoC.
- MultiBlocProvider lets you inject multiple BLoCs or Cubits at once, keeping state management clean as apps grow.
- The cart example includes add, remove, update quantity, and clear functionality, with UI updates via BlocBuilder.
- ThemeCubit toggles between light and dark modes, while AuthBloc simulates login/logout, showing how different state types work together.
- The Todo app demonstrates event-driven workflows with Add, Edit, Delete, Toggle, and Filter (All, Active, Completed) operations.
- Key BLoC concepts covered include BlocProvider for injection, BlocBuilder for rebuilding UI, context.read() to access without listening, and context.watch() to listen and rebuild.
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.
