Flutter BLoC Shopping Cart | Update List & Map

Here we will focus how to create simple shopping cart using BLoC and we will also see how to update List or Map like objects using BLoC

This should be more advanced than our counter Bloc App. If you want to about simple Getx shopping Cart click here.

Make sure you have flutter_bloc iand equtable packages nstalled. 

BLoC States

First we will create state class for our shopping cart. It will hold data in a List. This list would be updated as we click on the button.

Let's create a state class and name it app_cartstate.dart

abstract class CartState {
  final List<int> cartItem;
  const CartState({@required this.cartItem});

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

class CartLoadInProgress extends CartState {}

class ProductAdded extends CartState {
  final List<int> cartItem;

  const ProductAdded({@required this.cartItem}) : super(cartItem: cartItem);

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

  @override
  String toString() => 'ProductAdded { todos: $cartItem }';
}

class ProductRemoved extends CartState {
  final List<int> cartItem;

  const ProductRemoved({@required this.cartItem}) : super(cartItem: cartItem);

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

  @override
  String toString() => 'ProductRemoved { todos: $cartItem }';
}

Our simple state class CartState holds a List. It's int type, because we will store number of items added in the List.

CartState class is an abstract class which is extended by ProductAdded and ProductRemoved class. Those classes are called (to emit) as we click on the product item to add or remove.

The only props this classes have is cartItem. This is the List which will be accessed from UI to know how many items are in the cart.

So cartItem is the List we will be updating using BLoC.

BLoC Events

It's time to create events, which will cause states to be emitted. We just made our states. State classes update our List and event classes let us know how many interaction has been done in the UI.

Let's create an event class and name it app_events.dart

abstract class CartEvent extends Equatable {
  const CartEvent();

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

class AddProduct extends CartEvent {
  final int productIndex;

  const AddProduct(this.productIndex);

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

  @override
  String toString() => 'AddProduct { index: $productIndex }';
}

class RemoveProduct extends CartEvent {
  final int productIndex;

  const RemoveProduct(this.productIndex);

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

  @override
  String toString() => 'RemoveProduct { index: $productIndex }';
}

Our CartEvent class is an abstract class and it extends Equatable class. With this Equatable class, we would be able to know if two events are similar or not.

Here we see two class AddProduct and RemoveProduct. These classes help us to know if events are added or removed from the event list. It happens internally with BLoC.

When events are called( added to the event list) then we emit() states.

Each time an item of product button is pressed, an event would be triggered. Then we will emit states.

Create BLoCs

Now it's time to combine states and events in one class to create our BLoCs. We can also say BLoc class.

You can only create blocs if you have states and events.

Let's create a bloc class and name app_blocs.dart

class CartBloc extends Bloc<CartEvent, CartState> {
  CartBloc() : super(ProductAdded(cartItem: [])){

    on<AddProduct>((event, emit){
      _cartItems.add(event.productIndex);
       emit(ProductAdded(cartItem: _cartItems));
    });

    on<RemoveProduct>((event, emit){
      _cartItems.remove(event.productIndex);
       emit(ProductRemoved(cartItem: _cartItems));
    });
  }

  final List<int> _cartItems = [];
  List<int> get items => _cartItems;

}

Our CartBloc class extends BLoC class and it takes of type events and states. So we are passing event class CartEvent and state class CartState

In the contructor body of this, we pass arguments for super class contructor.

In the supe class contructor we are passing ProdcutAdded class with initial List value []

With this we will have cartItem List would be initialized to empty List.

Here we used method on() and mention the type of the event. So you will see on<AddProduct> and on<RemoveProduct>.

Those two methods first add events as you can see _cartItem.add(event.productIndex)

Since we have access to event object, we can access the related property of that class.

With this we see if two events are same or not.  Because event class extends Equutable class, we can know about two events and compare. It happens automitically. 

Inject BloC (Dependency Injection)

Now we will create our UI. In our homepage ui, we will inject our BLoC using BlocProvider and use BlocBuilder widget to access the blocs. 

If you can access blocs, you would be able to access events and states.

BlocProvider takes our CartBloc class and use the create property to create BloCs for us.

Let's create a class called home_page.dart

class HomePage extends StatefulWidget {
  static String routeName = '/';

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  double _crossAxisSpacing = 8, _mainAxisSpacing = 12, _aspectRatio = 5;

  int _crossAxisCount = 1;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    double screenWidth = MediaQuery.of(context).size.width;

    var width = (screenWidth - ((_crossAxisCount - 1) * _crossAxisSpacing)) /
        _crossAxisCount;
    var height = width / _aspectRatio;

    return BlocProvider(
        create: (_) => CartBloc(),
        child: Scaffold(
          appBar: AppBar(
            title: Text('Shopping App'),
            actions: <Widget>[
              Stack(
                children: [
                  Padding(
                    padding: const EdgeInsets.only(right: 16),
                    child: TextButton.icon(
                      style: TextButton.styleFrom(primary: Colors.white),
                      onPressed: () {
                        //Navigator.pushNamed(context, CartPage.routeName);
                      },
                      icon: Icon(Icons.shopping_cart),
                      label: Text(''),
                      key: Key('cart'),
                    ),
                  ),
                  BlocBuilder<CartBloc, CartState>(builder: (_, cartState) {
                    List<int> cartItem = cartState.cartItem;
                    return Positioned(
                      left: 30,
                      child: Container(
                        padding: EdgeInsets.all(5),
                        decoration: BoxDecoration(
                            borderRadius: BorderRadius.circular(10),
                            color: Colors.red),
                        child: Text(
                          '${cartItem.length}',
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                      ),
                    );
                  }),
                ],
              ),
            ],
          ),
          body: ProductList(),
        ));
  }
}

BlocBuilder can access the states and check them. It would be able to access any property of the state classes.

As you can see we accessed cartItem using cartState.cartItem.

We wanna create a new class called product_list.dart

class ProductList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<CartBloc, CartState>(builder: (_, cartState) {
      List<int> cart = cartState.cartItem;
      return LayoutBuilder(builder: (context, constraints) {
        return GridView.builder(
          itemCount: 100,
          itemBuilder: (context, index) => ProductTile(
            itemNo: index,
            cart: cart,
          ),
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: constraints.maxWidth > 700 ? 4 : 1,
            childAspectRatio: 5,
          ),
        );
      });
    });
  }
}

Here we access the whole list from the state class using cartState.cartItem and pass to ProductTile class.

Let's create a class product_tile.dart

class ProductTile extends StatelessWidget {
  final int itemNo;
  final List<int> cart;

  const ProductTile({this.itemNo, this.cart});

  @override
  Widget build(BuildContext context) {
    final Color color = Colors.primaries[itemNo % Colors.primaries.length];
    var cartList = BlocProvider.of<CartBloc>(context).items;
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        onTap: () {
          Navigator.pushNamed(
            context,
            ProductPage.routeName,
            arguments: Product('Product $itemNo', color),
          );
        },
        leading: Container(
          width: 50,
          height: 30,
          child: Placeholder(
            color: color,
          ),
        ),
        title: Text(
          'Product $itemNo',
          key: Key('text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('icon_$itemNo'),
          icon: cart.contains(itemNo)
              ? Icon(Icons.shopping_cart)
              : Icon(Icons.shopping_cart_outlined),
          onPressed: () {
            !cart.contains(itemNo)
                ? BlocProvider.of<CartBloc>(context).add(AddProduct(itemNo))
                : BlocProvider.of<CartBloc>(context).add(RemoveProduct(itemNo));
          },
        ),
      ),
    );
  }
}

ProductTile class access our BloC and it's property items. The property items holds our triggered events on the button click.

Remember earlier we added events in a List. Here we check if an event is already in the List or not. 

That's why we use contains() function of List.

Let's create the main.dart class

void main() {

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.teal,
        brightness: Brightness.dark,
      ),
      routes: {
        HomePage.routeName: (context) => HomePage(),
        //CartPage.routeName: (context) => CartPage(),
      },
      initialRoute: HomePage.routeName,
      //home: MyBookings(),
      debugShowCheckedModeBanner: false,
    );
  }
}

Learn about more advanced BLoC list item update here

Recent posts