Mohamed Elsheikh
8 min readJul 22, 2023

Flutter Bloc shopping cart app — Part 1

In the world of Flutter, there are numerous tutorials on the basics of BLOC (Business Logic Component) architecture. However, most of them focus on single BLOC examples or simple use cases. As developers, we often encounter more complex scenarios in real-world applications where multiple BLOCs need to work harmoniously together.

This tutorial series aims to bridge that gap and provide a more in-depth understanding of BLOC architecture with realistic, complex examples.

This series of tutorials will be for that , We will be reusing examples written by Felix Angelov in the main bloc repository.

This series will be shared on multiple parts

Part 1 : The shopping cart app

If you want to access the end code of this tutorial , you can check it on github

Modifying the shopping cart app

  • Adding Quantity: Enhance the shopping items by allowing users to select a quantity for each item they wish to purchase.
  • Modifying BLOC Logic: Improve the BLOC logic to handle the updated quantity of each item in the shopping cart.
  • UI Enhancements: Modify the user interface to include buttons for adding and removing items, as well as displaying the selected items with their respective quantities.

Part 2:

We will be modifying the second screen of this exmaple which is the shopping cart to support the quantity and the correct total pricing

part3

We will add the login screen from another example , profile screen and log out button

Part4 :

We will add Presistency features , User will log out and log back in to still find his items in the cart

So please Follow this page so you can be notified of the next parts.

Now let’s get to work on Part 1

https://github.com/felangel/bloc/tree/master/examples

First I will download the bloc repo alltogether to my computer Then I will copy the flutter_shopping_cart to a new folder .

This will be our working space

Go to the pubspec.yaml and under the dev_dependences delete the bloc , flutter_bloc and bloc_test as we will already get them from the pub.dev instead of the local paths

First let’s run the shopping cart example :

Let’s do some improvements on this example

You can notice we can only add one item per product , what if we need to add multiple items of the same product.

So let’s do that :

Go to models/cart.dart

This will be our starting point

You will notice that the cart has only a list of ‘items’

  final List<Item> items;

let’s add a map that includes each shopping item and its quantity

 class Cart extends Equatable {
const Cart({this.items = const <Item>[] , this.itemsWithQuantity = const {} });

final List<Item> items;
final Map<Item, int> itemsWithQuantity;

int get totalPrice {
return items.fold(0, (total, current) => total + current.price);
}

@override
List<Object> get props => [items];
}

In the shopping_repository.dart ,Lets add our

shopping lists map(items with quantity)

addItemToCartwQuantity : which should increment the quantity by 1 if the product is already in the cart or just put 1 if the product didn’t exist

removeItemFromCartwQuantity : which should decrement the quantity by 1 if the product is already in the cart or just enforce a 0 if the product doesn’t exist

class ShoppingRepository {
final _items = <Item>[];
final itemsWithQuantity = <Item,int>{};

Future<List<String>> loadCatalog() => Future.delayed(_delay, () => _catalog);

Future<List<Item>> loadCartItems() => Future.delayed(_delay, () => _items);
Future<Map<Item,int>> loadCartItemsWithQuantity() => Future.delayed(_delay, () => itemsWithQuantity);

void addItemToCart(Item item) => _items.add(item);

void addItemToCartwQuantity(Item item) => itemsWithQuantity.update(item, (oldvalue) => oldvalue+1 , ifAbsent: ()=>1) ;

void removeItemFromCart(Item item) => _items.remove(item);

void removeItemFromCartwQuantity(Item item) => itemsWithQuantity.update(item, (oldvalue) => oldvalue-1 , ifAbsent: ()=>0) ;
}

Next let’s modify our cart_bloc.dart

We need to modify three functions here

_OnStarted : we added our map and called our repository function that we have just created

As well as passing in the itemsWithQuantity map to the cart inside when we emit the state

Future<void> _onStarted(CartStarted event, Emitter<CartState> emit) async {
emit(CartLoading());
try {
final items = await shoppingRepository.loadCartItems();
final itemsWithQuantity = await shoppingRepository.loadCartItemsWithQuantity();
emit(CartLoaded(cart: Cart(items: [...items] , itemsWithQuantity: itemsWithQuantity )));
} catch (_) {
emit(CartError());
}
}

similarly with _onItemAdded

Future<void> _onItemAdded(
CartItemAdded event,
Emitter<CartState> emit,
) async {
final state = this.state;
if (state is CartLoaded) {
try {
shoppingRepository.addItemToCart(event.item);
shoppingRepository.addItemToCartwQuantity(event.item);

// state.cart.itemswithquantity.update(event.item, (value) => value+1 , ifAbsent: () => 1);
emit(CartLoaded(cart: Cart(items: [...state.cart.items, event.item],
itemsWithQuantity: state.cart.itemsWithQuantity
)));
} catch (_) {
emit(CartError());
}
}
}

and _onItemRemoved :

  void _onItemRemoved(CartItemRemoved event, Emitter<CartState> emit) {
final state = this.state;
if (state is CartLoaded) {
try {
shoppingRepository.removeItemFromCart(event.item);
shoppingRepository.removeItemFromCartwQuantity(event.item);
emit(
CartLoaded(
cart: Cart(
items: [...state.cart.items]..remove(event.item),
itemsWithQuantity: state.cart.itemsWithQuantity
),
),
);
} catch (_) {
emit(CartError());
}
}
}

Now we have finished our logic work. Its time to modify our UI

We will aim to have something like that

So in catalog_page.dart lets go all the way down to ‘CatalogListItem’

And add two new widgets:

quantityDisplayed(item: item),
RemoveButton(item: item),

class CatalogListItem extends StatelessWidget {
const CatalogListItem(this.item, {super.key});

final Item item;

@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme.titleLarge;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: LimitedBox(
maxHeight: 48,
child: Row(
children: [
AspectRatio(aspectRatio: 1, child: ColoredBox(color: item.color)),
const SizedBox(width: 24),
Expanded(child: Text(item.name, style: textTheme)),
const SizedBox(width: 24),
AddButton(item: item),
quantityDisplayed(item: item),
RemoveButton(item: item),

],
),
),
);
}
}

First lets start our surgery by cleaning up the AddButton widget

We will delete everything here and leave only an Icon of a ‘+’


return TextButton(
style: TextButton.styleFrom(
disabledForegroundColor: theme.primaryColor,
),
onPressed: isInCart
? null
: () => context.read<CartBloc>().add(CartItemAdded(item)),
child: isInCart
? const Icon(Icons.check, semanticLabel: 'ADDED')
: const Text('ADD'),
);

So Here’s how it will look like :

class AddButton extends StatelessWidget {
const AddButton({required this.item, super.key});

final Item item;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
return switch (state) {
CartLoading() => const CircularProgressIndicator(),
CartError() => const Text('Something went wrong!'),
CartLoaded() => Builder(
builder: (context) {
final isInCart = state.cart.items.contains(item);
return TextButton(
style: TextButton.styleFrom(
disabledForegroundColor: theme.primaryColor,
),
onPressed:

() => context.read<CartBloc>().add(CartItemAdded(item)),
child:
const Icon(Icons.add, semanticLabel: 'Add')

);
},
)
};
},
);
}
}

Lets resue the AddButton with our nre RemoveButton :

class RemoveButton extends StatelessWidget {
const RemoveButton({required this.item, super.key});

final Item item;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
return switch (state) {
CartLoading() => const CircularProgressIndicator(),
CartError() => const Text('Something went wrong!'),
CartLoaded() => Builder(
builder: (context) {
final isInCart = state.cart.items.contains(item);
return TextButton(
style: TextButton.styleFrom(
disabledForegroundColor: theme.primaryColor,
),
onPressed:
() => context.read<CartBloc>().add(CartItemRemoved(item)),
child:
// ? const Icon(Icons.check, semanticLabel: 'Remove')
// ? Text('+ ${state.cart.itemsWithQuantity[item]}')
const Icon(Icons.remove, semanticLabel: 'Remove'),
);
},
)
};
},
);
}
}

Then our quantityDisplayed widget : which should be displayed in the middle between the + and (-) buttons

class quantityDisplayed extends StatelessWidget {
const quantityDisplayed({required this.item, super.key});

final Item item;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
return switch (state) {
CartLoading() => const CircularProgressIndicator(),
CartError() => const Text('Something went wrong!'),
CartLoaded() => Builder(
builder: (context) {
return
Text('${state.cart.itemsWithQuantity[item]}');
},
)
};
},
);
}
}

Now let’s restart our app and see :

Looks like the job is done , However 0 should be displayed instead of null ,

The easiest way is to use the null colascening operator ?? and provide 0 as default value

class quantityDisplayed extends StatelessWidget {
const quantityDisplayed({required this.item, super.key});

final Item item;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
return switch (state) {
CartLoading() => const CircularProgressIndicator(),
CartError() => const Text('Something went wrong!'),
CartLoaded() => Builder(
builder: (context) {
return
Text('${state.cart.itemsWithQuantity[item]??0}');
},
)
};
},
);
}
}

Now we can see default zero has been displayed

However there is a new problem that I noticed :

When the app restarts there are redundant loaders for each of the add and remove buttons , so let’s clean and optimise this

All we need is to remove the switch state from the Add and Remove buttons

class RemoveButton extends StatelessWidget {
const RemoveButton({required this.item, super.key});

final Item item;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
return Builder(
builder: (context) {
return TextButton(
style: TextButton.styleFrom(
disabledForegroundColor: theme.primaryColor,
),
onPressed:
() => context.read<CartBloc>().add(CartItemRemoved(item)),
child:
// ? const Icon(Icons.check, semanticLabel: 'Remove')
// ? Text('+ ${state.cart.itemsWithQuantity[item]}')
const Icon(Icons.remove, semanticLabel: 'Remove'),
);
},
);

},
);
}
}
class AddButton extends StatelessWidget {
const AddButton({required this.item, super.key});

final Item item;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
return
TextButton(
style: TextButton.styleFrom(
disabledForegroundColor: theme.primaryColor,
),
onPressed:
// isInCart
// ? () => context.read<CartBloc>().add(CartItemRemoved(item))
// :
() => context.read<CartBloc>().add(CartItemAdded(item)),
child:
// isInCart
// ?
const Icon(Icons.add, semanticLabel: 'Add')
// ? Text('+ ${state.cart.itemsWithQuantity[item]}')
// : const Text('ADD'),
);
},
);
}
}

Now much better :

Now let’s ask ourselves is our code really optimised in the best way or is there better?

The answer lies with our usage of the BlocBuilder in the three widgets

Do we need to build the Add and Remove buttons everytime the state changes ? No we don’t

so we should use the BlocListener instead

let’s do that , this will be the last thing in this tutorial

In the RemoveButton and the AddButton ,do the following

class RemoveButton extends StatelessWidget {
const RemoveButton({required this.item, super.key});

final Item item;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocListener<CartBloc, CartState>(
listener: (context, state) {},
child: TextButton(
style: TextButton.styleFrom(
disabledForegroundColor: theme.primaryColor,
),
onPressed: () => context.read<CartBloc>().add(CartItemRemoved(item)),
child:
const Icon(Icons.remove, semanticLabel: 'Remove'),
));
}
}

I hope this was useful to everyone who has followed , if you want to download the code please check

Follow me for part 2.