image

21 Mar 2026

9K

35K

Building a Shopping Cart Widget with Editable Quantity and Promo Code in Flutter

A robust shopping cart is a cornerstone of any e-commerce mobile application. It allows users to manage their selected items, adjust quantities, and apply discounts, all before proceeding to checkout. In Flutter, building such a dynamic and interactive widget requires careful consideration of state management and UI design. This article will guide you through creating a sophisticated shopping cart widget featuring editable item quantities and a functional promo code input.

1. Setting Up the Project and Dependencies

We'll use the provider package for state management, as it's a popular and relatively simple choice for many Flutter applications. First, add it to your pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  provider: ^6.0.5 # Or the latest version

Run flutter pub get to fetch the dependency.

2. Data Models

We need simple models to represent our products and the items within the cart. A Product will have basic information, and a CartItem will link to a product and hold its quantity.

product.dart


import 'package:flutter/foundation.dart';

class Product {
  final String id;
  final String name;
  final double price;
  final String imageUrl;

  Product({
    required this.id,
    required this.name,
    required this.price,
    required this.imageUrl,
  });

  // Example factory constructor for dummy data
  factory Product.dummy(String id) {
    return Product(
      id: id,
      name: 'Product $id',
      price: double.parse(id) * 10.0,
      imageUrl: 'https://via.placeholder.com/150/0000FF/FFFFFF?text=P$id',
    );
  }
}

cart_item.dart


import 'package:flutter_shopping_cart/models/product.dart';

class CartItem {
  final Product product;
  int quantity;

  CartItem({
    required this.product,
    this.quantity = 1,
  });

  // Method to get total price for this item
  double get totalPrice => product.price * quantity;
}

3. State Management with CartProvider

The CartProvider will extend ChangeNotifier and manage the entire state of our shopping cart. It will handle adding, removing, updating item quantities, and applying promo codes.

cart_provider.dart


import 'package:flutter/material.dart';
import 'package:flutter_shopping_cart/models/cart_item.dart';
import 'package:flutter_shopping_cart/models/product.dart';

class CartProvider with ChangeNotifier {
  Map<String, CartItem> _items = {};
  String? _promoCode;
  double _discountPercentage = 0.0;

  Map<String, CartItem> get items => {..._items}; // Return a copy
  List<CartItem> get cartItems => _items.values.toList();
  int get itemCount => _items.length;
  String? get currentPromoCode => _promoCode;

  double get subtotalAmount {
    double total = 0.0;
    _items.forEach((key, cartItem) {
      total += cartItem.totalPrice;
    });
    return total;
  }

  double get discountAmount {
    return subtotalAmount * _discountPercentage;
  }

  double get totalAmount {
    return subtotalAmount - discountAmount;
  }

  void addItem(Product product) {
    if (_items.containsKey(product.id)) {
      _items.update(
        product.id,
        (existingItem) => CartItem(
          product: existingItem.product,
          quantity: existingItem.quantity + 1,
        ),
      );
    } else {
      _items.putIfAbsent(
        product.id,
        () => CartItem(product: product),
      );
    }
    notifyListeners();
  }

  void removeItem(String productId) {
    _items.remove(productId);
    notifyListeners();
  }

  void updateItemQuantity(String productId, int quantity) {
    if (quantity <= 0) {
      removeItem(productId);
      return;
    }
    _items.update(
      productId,
      (existingItem) => CartItem(
        product: existingItem.product,
        quantity: quantity,
      ),
    );
    notifyListeners();
  }

  void applyPromoCode(String code) {
    _promoCode = code;
    // Simple promo code logic for demonstration
    if (code.toUpperCase() == 'DISCOUNT10') {
      _discountPercentage = 0.10; // 10% discount
    } else if (code.toUpperCase() == 'FREESHIP') {
      _discountPercentage = 0.0; // Example for a different type of promo (e.g., free shipping)
                                 // For this example, we'll only apply a percentage discount.
    } else {
      _discountPercentage = 0.0; // No discount for invalid codes
    }
    notifyListeners();
  }

  void removePromoCode() {
    _promoCode = null;
    _discountPercentage = 0.0;
    notifyListeners();
  }

  void clearCart() {
    _items = {};
    _promoCode = null;
    _discountPercentage = 0.0;
    notifyListeners();
  }
}

main.dart (Provider Setup)

Wrap your app with ChangeNotifierProvider to make the CartProvider accessible throughout the widget tree.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_shopping_cart/providers/cart_provider.dart';
import 'package:flutter_shopping_cart/screens/shopping_cart_screen.dart';
import 'package:flutter_shopping_cart/models/product.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => CartProvider(),
      child: MaterialApp(
        title: 'Flutter Shopping Cart',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: ProductListScreen(), // Or directly ShoppingCartScreen for testing
      ),
    );
  }
}

// A simple product list screen to add items to cart for testing
class ProductListScreen extends StatelessWidget {
  final List<Product> _dummyProducts = [
    Product.dummy('1'),
    Product.dummy('2'),
    Product.dummy('3'),
    Product.dummy('4'),
  ];

  @override
  Widget build(BuildContext context) {
    final cart = Provider.of<CartProvider>(context, listen: false);
    return Scaffold(
      appBar: AppBar(
        title: Text('Products'),
        actions: [
          Consumer<CartProvider>(
            builder: (_, cartData, ch) => Badge(
              label: Text(cartData.itemCount.toString()),
              child: IconButton(
                icon: Icon(Icons.shopping_cart),
                onPressed: () {
                  Navigator.of(context).push(
                    MaterialPageRoute(builder: (ctx) => ShoppingCartScreen()),
                  );
                },
              ),
            ),
          )
        ],
      ),
      body: ListView.builder(
        itemCount: _dummyProducts.length,
        itemBuilder: (ctx, i) {
          final product = _dummyProducts[i];
          return Card(
            margin: EdgeInsets.all(8),
            child: ListTile(
              leading: Image.network(product.imageUrl, width: 50, height: 50),
              title: Text(product.name),
              subtitle: Text('\$${product.price.toStringAsFixed(2)}'),
              trailing: IconButton(
                icon: Icon(Icons.add_shopping_cart),
                onPressed: () {
                  cart.addItem(product);
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Added ${product.name} to cart!')),
                  );
                },
              ),
            ),
          );
        },
      ),
    );
  }
}

// Simple Badge widget (can be replaced by official Badge if available or custom)
class Badge extends StatelessWidget {
  final Widget child;
  final Widget label;

  Badge({required this.child, required this.label});

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: [
        child,
        Positioned(
          right: 8,
          top: 8,
          child: Container(
            padding: EdgeInsets.all(2.0),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(10.0),
              color: Colors.red,
            ),
            constraints: BoxConstraints(
              minWidth: 16,
              minHeight: 16,
            ),
            child: Center(
              child: label,
            ),
          ),
        )
      ],
    );
  }
}

4. Building the Cart Item Widget

This widget displays a single item in the cart, along with its quantity and controls to adjust it.

cart_item_widget.dart


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_shopping_cart/models/cart_item.dart';
import 'package:flutter_shopping_cart/providers/cart_provider.dart';

class CartItemWidget extends StatelessWidget {
  final CartItem cartItem;

  CartItemWidget({required this.cartItem});

  @override
  Widget build(BuildContext context) {
    final cart = Provider.of<CartProvider>(context, listen: false);

    return Dismissible(
      key: ValueKey(cartItem.product.id),
      direction: DismissDirection.endToStart,
      background: Container(
        color: Colors.red,
        alignment: Alignment.centerRight,
        padding: EdgeInsets.only(right: 20),
        margin: EdgeInsets.symmetric(
          horizontal: 15,
          vertical: 4,
        ),
        child: Icon(
          Icons.delete,
          color: Colors.white,
          size: 40,
        ),
      ),
      confirmDismiss: (direction) {
        return showDialog(
          context: context,
          builder: (ctx) => AlertDialog(
            title: Text('Are you sure?'),
            content: Text('Do you want to remove the item from the cart?'),
            actions: [
              TextButton(
                child: Text('No'),
                onPressed: () {
                  Navigator.of(ctx).pop(false);
                },
              ),
              TextButton(
                child: Text('Yes'),
                onPressed: () {
                  Navigator.of(ctx).pop(true);
                },
              ),
            ],
          ),
        );
      },
      onDismissed: (direction) {
        cart.removeItem(cartItem.product.id);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('${cartItem.product.name} removed from cart!')),
        );
      },
      child: Card(
        margin: EdgeInsets.symmetric(
          horizontal: 15,
          vertical: 4,
        ),
        child: Padding(
          padding: EdgeInsets.all(8),
          child: ListTile(
            leading: CircleAvatar(
              backgroundImage: NetworkImage(cartItem.product.imageUrl),
              radius: 30,
            ),
            title: Text(cartItem.product.name),
            subtitle: Text(
              'Total: \$${cartItem.totalPrice.toStringAsFixed(2)}',
            ),
            trailing: Column(
              children: [
                Spacer(),
                Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    IconButton(
                      icon: Icon(Icons.remove_circle_outline),
                      onPressed: () {
                        cart.updateItemQuantity(
                            cartItem.product.id, cartItem.quantity - 1);
                      },
                    ),
                    Text(cartItem.quantity.toString()),
                    IconButton(
                      icon: Icon(Icons.add_circle_outline),
                      onPressed: () {
                        cart.updateItemQuantity(
                            cartItem.product.id, cartItem.quantity + 1);
                      },
                    ),
                  ],
                ),
                Text('\$${cartItem.product.price.toStringAsFixed(2)} / item'),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

5. Building the Shopping Cart Screen

This screen will display all cart items, the promo code input, and the total summary.

shopping_cart_screen.dart


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_shopping_cart/providers/cart_provider.dart';
import 'package:flutter_shopping_cart/widgets/cart_item_widget.dart';

class ShoppingCartScreen extends StatefulWidget {
  @override
  _ShoppingCartScreenState createState() => _ShoppingCartScreenState();
}

class _ShoppingCartScreenState extends State<ShoppingCartScreen> {
  final _promoCodeController = TextEditingController();

  @override
  void dispose() {
    _promoCodeController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final cart = Provider.of<CartProvider>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('My Cart'),
      ),
      body: Column(
        children: [
          Expanded(
            child: cart.itemCount == 0
                ? Center(child: Text('Your cart is empty!'))
                : ListView.builder(
                    itemCount: cart.itemCount,
                    itemBuilder: (ctx, i) => CartItemWidget(
                      cartItem: cart.cartItems[i],
                    ),
                  ),
          ),
          Divider(),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text('Subtotal:', style: TextStyle(fontSize: 18)),
                    Text('\$${cart.subtotalAmount.toStringAsFixed(2)}',
                        style: TextStyle(fontSize: 18)),
                  ],
                ),
                SizedBox(height: 10),
                Row(
                  children: [
                    Expanded(
                      child: TextField(
                        controller: _promoCodeController,
                        decoration: InputDecoration(
                          labelText: 'Promo Code',
                          border: OutlineInputBorder(),
                        ),
                      ),
                    ),
                    SizedBox(width: 10),
                    ElevatedButton(
                      onPressed: () {
                        cart.applyPromoCode(_promoCodeController.text);
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('Promo code applied!')),
                        );
                      },
                      child: Text('Apply'),
                      style: ElevatedButton.styleFrom(
                        padding: EdgeInsets.symmetric(vertical: 15),
                      ),
                    ),
                  ],
                ),
                SizedBox(height: 10),
                if (cart.currentPromoCode != null && cart.discountAmount > 0)
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text('Discount (${cart.currentPromoCode!})',
                          style: TextStyle(fontSize: 18, color: Colors.green)),
                      Text('-\$${cart.discountAmount.toStringAsFixed(2)}',
                          style: TextStyle(fontSize: 18, color: Colors.green)),
                    ],
                  ),
                SizedBox(height: 10),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text('Total:',
                        style: TextStyle(
                            fontSize: 22, fontWeight: FontWeight.bold)),
                    Text('\$${cart.totalAmount.toStringAsFixed(2)}',
                        style: TextStyle(
                            fontSize: 22, fontWeight: FontWeight.bold)),
                  ],
                ),
                SizedBox(height: 20),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    onPressed: cart.totalAmount <= 0 ? null : () {
                      // Implement checkout logic here
                      cart.clearCart(); // Clear cart after checkout
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text('Checkout successful! Cart cleared.')),
                      );
                      Navigator.of(context).pop();
                    },
                    child: Text('Checkout', style: TextStyle(fontSize: 20)),
                    style: ElevatedButton.styleFrom(
                      padding: EdgeInsets.symmetric(vertical: 15),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

6. Key Features and Implementation Details

  • Editable Quantity

    In CartItemWidget, we use IconButtons with Icons.remove_circle_outline and Icons.add_circle_outline. Tapping these buttons calls cart.updateItemQuantity(cartItem.product.id, cartItem.quantity +/- 1), which triggers a UI rebuild through notifyListeners() in CartProvider.

    A Dismissible widget is used to allow users to remove items by swiping them away, with a confirmation dialog for better UX.

  • Promo Code Application

    The ShoppingCartScreen includes a TextField for entering a promo code and an "Apply" button. When the button is pressed, cart.applyPromoCode(_promoCodeController.text) is called.

    The applyPromoCode method in CartProvider contains a simple conditional logic (e.g., 'DISCOUNT10' for a 10% discount). In a real application, this logic would typically involve API calls to validate promo codes and fetch discount details from a backend.

    The discountAmount and totalAmount getters automatically recalculate whenever the cart items or the discount percentage changes, ensuring the summary is always up-to-date.

  • Dynamic Pricing Summary

    The shopping cart screen dynamically displays the subtotal, applied discount (if any), and the final total amount. These values are computed getters in the CartProvider, ensuring they always reflect the current state of the cart.

  • Checkout Button

    A checkout button is provided, which is disabled if the cart total is zero or less. Upon checkout, a simple action like clearing the cart is demonstrated, but this is where your actual payment processing and order submission logic would reside.

7. Enhancements and Considerations

  • Persistence: For a real application, you'd want to persist the cart's state using local storage (e.g., SharedPreferences, Hive) or a database, so items aren't lost when the app closes.
  • Error Handling: Implement robust error handling for promo code validation (e.g., invalid code, expired code, minimum order value not met).
  • Different Discount Types: Extend the applyPromoCode logic to handle various discount types (e.g., fixed amount discount, free shipping, buy-one-get-one offers).
  • Item Availability: Integrate checks for product stock availability when updating quantities.
  • User Authentication: Link the cart to a specific user for personalized shopping experiences and server-side cart management.
  • Loading States: When applying a promo code that requires a backend call, show a loading indicator.
  • Animations: Add subtle animations for item additions/removals or quantity changes to enhance user experience.

Conclusion

By leveraging Flutter's reactive UI framework and a robust state management solution like Provider, you can build a highly interactive and user-friendly shopping cart widget. This guide covered the fundamental aspects, from data modeling and state management to UI implementation for editable quantities and promo code functionality. With these building blocks, you are well-equipped to expand and customize your shopping cart to meet the specific demands of your e-commerce application.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is