Published
- 7 min read
Flutter Bloc: A complete guide to handle state management with cart functionality
Overview
Flutter Bloc is a well-architectured way to manage global state management in applications. Bloc makes it easy to separate the business logic that makes your code easy to test and reuse. In this post, I will write a step-by-step guide to implementing Flutter Bloc to build a cart functionality in the Flutter app. I will implement a basic cart feature where users can add products to the cart.
Create Flutter App
First, Let’s initialize the project by running flutter create mystore. Make sure that you have already installed flutter sdk in your environment. Now, add install bloc and flutter libraries in the project by running flutter pub add bloc flutter_bloc. We also need some mock products and the product model class. Create a new file at main/models/product_model.dart
and add the following code.
Create Product Model
class ProductModel {
final String id;
final String name;
final int price;
final String image;
ProductModel({ required this.id, required this.name, required this.price, required this.image });
ProductModel copyWith({
String? id,
String? name,
int? price,
String? image,
}) {
return ProductModel(
id: id ?? this.id,
name: name ?? this.name,
price: price ?? this.price,
image: image ?? this.image
);
}
}
Product Repository
main/repository/product_repository.dart
import 'dart:async';
import 'package:mystore/models/product_model.dart';
List < ProductModel > _products =[
ProductModel(id: '1', name: 't-shirt', price: 100, image: 't-shirt.jpg'),
ProductModel(id: '2', name: 'Bag', price: 150, image: 'bag.jpg'),
ProductModel(id: '3', name: 'Glasses', price: 20, image: 'glasses.jpg'),
ProductModel(id: '4', name: 'Jeans', price: 150, image: 'jeans.jpg'),
ProductModel(id: '5', name: 'Shoes', price: 160, image: 'shoe.jpg'),
ProductModel(id: '6', name: 'White Cap', price: 85, image: 'cap.jpg'),
];
class ProductRepository {
Future<List<ProductModel>> getProducts() async{
//fake loading
final products = await Future.delayed(const Duration(seconds: 1)).then((value) => _products);
return products;
}
}
Implementing Bloc
Now, Let’s define two cubits, products and cart to manage the state of both units separately. Blocs library uses blocs or cubits to define and manage the state of a unit. I’m using cubits instead of blocs because it’s easy and simple. You can even use cubits to handle complex states.
Product Cubit
Create main/state/product_cubit.dart
. We’ll define two classes in this file. ProductState class and ProductStateCubit class. ProductState class contains the state of products along with the data members to hold the properties while ProductStateCubit is used to interact with the repository and emit the changes to the state.
import 'package:bloc/bloc.dart';
import 'package:mystore/models/product_model.dart';
import 'package:mystore/repository/product_repository.dart';
enum AppStatus{initial, loading, loaded, error}
class ProductState {
final AppStatus status;
final List<ProductModel> products;
final String? error;
ProductState({
required this.status,
required this.products,
required this.error
});
ProductState copyWith({
AppStatus? status,
List<ProductModel>? products,
String? error
}) => ProductState(
status: status ?? this.status,
products: products ?? this.products,
error: error ?? this.error
);
}
class ProductStateCubit extends Cubit<ProductState> {
final ProductRepository productRepository;
ProductStateCubit({required this.productRepository}) : super(ProductState(status: AppStatus.initial, products: [], error: null));
Future<void> loadProducts() async {
try {
final products = await productRepository.getProducts();
emit(state.copyWith(status: AppStatus.loaded, products: products));
} catch (e) {
emit(state.copyWith(status: AppStatus.error, error: e.toString()));
}
}
}
Cart Cubit
The cart cubit will be responsible for managing the state of the cart page. Create main/state/cart_cubit.dart
. The cart cubit also contains two classes CartState and CartStateCubit. In CartState class, You can also add member functions for some extra functionalities, However, The member functions in this class will not update the state change until the emit function is not called after using the functions. Such as addProductToCart and removeProductFromCart will update the array of the state in memory but the UI will not be notified until a new state is not emitted from the CartStateCubit class.
import 'package:bloc/bloc.dart';
import 'package:mystore/models/product_model.dart';
class CartState {
final List<ProductModel> products;
CartState({ required this.products });
void addProductToCart(ProductModel product){
products.add(product);
}
void removeProductFromCart(ProductModel product){
products.remove(product);
}
bool isProductInCart(ProductModel product){
return products.contains(product);
}
int totlaPrice(){
return products.fold(0, (previousValue, element) => previousValue + element.price);
}
}
class CartStateCubit extends Cubit<CartState>{
CartStateCubit(): super(CartState(products: []));
void addProductToCart(ProductModel product){
state.addProductToCart(product);
// the ui will not rerender until the new state is not emitted,
emit(state);
}
void removeProductFromCart(ProductModel product){
state.removeProductFromCart(product);
emit(state);
}
}
UI Design
Let’s create the UI for the product view and cart view screen. We will also use bottom navigation to navigate between screens. Create main/product_view.dart
and main/cart_view.dart
Bottom Navigation
Go to main.dart
and update MyHomePage widget with the following code.
...
class MyHomePage extends StatefulWidget {
const MyHomePage({ super.key });
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final List<Widget> _pages = [ProductView(), CartView()];
int _selectedTab = 0;
void _changeTab(int index) {
setState(() {
_selectedTab = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My Store'),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedTab,
onTap: (index) => _changeTab(index),
selectedItemColor: Colors.red,
unselectedItemColor: Colors.grey,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: "Cart"),
],
),
body: _pages[_selectedTab],
);
}
}
The UI will look like this
Product View
Create a stateful widget with the name ProductView and Add
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mystore/models/product_model.dart';
import 'package:mystore/state/cart_cubit.dart';
import 'package:mystore/state/product_cubit.dart';
class ProductView extends StatefulWidget {
const ProductView({super.key});
@override
State<ProductView> createState() => _ProductViewState();
}
class _ProductViewState extends State<ProductView> {
@override
void initState() {
// load products
BlocProvider.of<ProductStateCubit>(context).loadProducts();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
),
body: BlocBuilder<ProductStateCubit, ProductState>(
builder: (context, state) {
if (state.status == AppStatus.loading) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Padding(
padding: const EdgeInsets.all(10.0),
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 3 / 4,
),
itemCount: state.products.length,
itemBuilder: (context, index) {
return ProductCard(
product: state.products[index],
);
},
),
);
},
),
);
}
}
class ProductCard extends StatelessWidget {
final ProductModel product;
const ProductCard({required this.product});
@override
Widget build(BuildContext context) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Image.asset(
product.image,
fit: BoxFit.cover,
width: double.infinity,
),
),
const SizedBox(height: 10),
Text(
product.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 5),
Text(
'\$${product.price}',
style: const TextStyle(
fontSize: 14,
color: Colors.green,
),
),
const SizedBox(height: 10),
Align(
alignment: Alignment.centerRight,
child: BlocBuilder<CartStateCubit, CartState>(
builder: (context, state) {
return IconButton(
onPressed: () {
if (state.isProductInCart(product)) {
state.removeProductFromCart(product);
} else {
state.addProductToCart(product);
}
},
icon: state.isProductInCart(product) ? Icon(Icons.remove_shopping_cart, color: Colors.red) : Icon(Icons.add_shopping_cart, color: Colors.blue),
);
},
),
),
],
),
),
);
}
}
Here BlocBuider is used to listen to the state from the product cubit. You can learn more about bloc builder here. We have also used BlocProvider.of(context) to get the reference of product cubit. Learn more about the BlocProvider here
The final UI will look like this
Cart View
Add the following code to main/cart_view.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mystore/state/cart_cubit.dart';
class CartView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Cart'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: BlocBuilder < CartStateCubit, CartState > (
builder: (context, state) {
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: state.products.length,
itemBuilder: (context, index) {
final item = state.products[index];
return Card(
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
title: Text(item.name),
subtitle: Text(
'Price: \$${item.price} x 1',
),
trailing: Text(
'\$${(item.price * 1).toStringAsFixed(2)}',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
);
},
),
),
const Divider(thickness: 2),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Total:',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'\$${state.totlaPrice()}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
),
],
);
},
),
),
);
}
}
The UI output will look like this
You have learned the use of bloc cubits in the flutter. You can get the source code of this project here