Multi Block
Block Pattern

Part 6 – Flutter BLoC Tutorial: BlocProvider & MultiBlocProvider Explained

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

Multi Block


🔹 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.

Leave a Reply

Your email address will not be published. Required fields are marked *