image

20 Apr 2026

9K

35K

Creating a Shopping Cart Widget with Editable Quantity, Promo Code, and Checkout in Flutter

A robust and user-friendly shopping cart is a cornerstone of any e-commerce application. It's where users review their selections, apply discounts, and initiate the purchase process. This article will guide you through building a comprehensive shopping cart widget in Flutter, featuring editable item quantities, promo code application, and a clear checkout flow.

1. Project Setup and Dependencies

First, ensure you have the provider package added to your pubspec.yaml for state management, which simplifies managing the cart's data globally.


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

2. Data Models

We'll need two main models: one for an individual product item and another for a cart item, which includes the product details and its quantity in the cart.

Product Model


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,
  });
}

Cart Item Model


class CartItem {
  final Product product;
  int quantity;

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

  double get totalPrice => product.price * quantity;
}

3. Cart State Management (CartProvider)

We'll use a ChangeNotifier class to manage the state of our shopping cart. This class will hold the list of CartItems, apply business logic for quantity updates, promo codes, and calculate totals.


import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; // For @required

// Assuming Product and CartItem models are defined as above

class CartProvider extends ChangeNotifier {
  final Map<String, CartItem> _items = {};
  String? _appliedPromoCode;
  double _discountPercentage = 0.0; // e.g., 0.10 for 10%

  Map<String, CartItem> get items => {..._items}; // Return a copy
  int get itemCount => _items.length;
  String? get appliedPromoCode => _appliedPromoCode;

  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, quantity: 1),
      );
    }
    notifyListeners();
  }

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

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

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

  double get discountAmount {
    return subtotal * _discountPercentage;
  }

  double get total {
    return subtotal - discountAmount;
  }

  void applyPromoCode(String promoCode) {
    // In a real app, this would involve API calls to validate and get discount
    // For this example, let's hardcode some promo codes
    if (promoCode.toUpperCase() == 'FLUTTER20') {
      _discountPercentage = 0.20; // 20% discount
      _appliedPromoCode = promoCode;
    } else if (promoCode.toUpperCase() == 'SAVE10') {
      _discountPercentage = 0.10; // 10% discount
      _appliedPromoCode = promoCode;
    } else {
      _discountPercentage = 0.0;
      _appliedPromoCode = null;
    }
    notifyListeners();
  }

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

  void clearCart() {
    _items.clear();
    _discountPercentage = 0.0;
    _appliedPromoCode = null;
    notifyListeners();
  }
}

4. Main Shopping Cart Widget

This widget will display the list of items, the promo code input, and the total summary with a checkout button.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// Assuming CartProvider, Product, and CartItem models are defined

class ShoppingCartScreen extends StatefulWidget {
  const ShoppingCartScreen({super.key});

  @override
  State<ShoppingCartScreen> createState() => _ShoppingCartScreenState();
}

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

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

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

    return Scaffold(
      appBar: AppBar(
        title: const Text('Your Shopping Cart'),
      ),
      body: Column(
        children: [
          // Cart Items List
          Expanded(
            child: cartProvider.itemCount == 0
                ? const Center(
                    child: Text(
                      'Your cart is empty!',
                      style: TextStyle(fontSize: 18),
                    ),
                  )
                : ListView.builder(
                    itemCount: cartProvider.itemCount,
                    itemBuilder: (ctx, i) {
                      final cartItem = cartProvider.items.values.toList()[i];
                      return CartItemWidget(cartItem: cartItem);
                    },
                  ),
          ),
          const Divider(),

          // Promo Code Input
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Have a promo code?',
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                const SizedBox(height: 8),
                Row(
                  children: [
                    Expanded(
                      child: TextField(
                        controller: _promoCodeController,
                        decoration: InputDecoration(
                          hintText: 'Enter promo code',
                          border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(8),
                          ),
                          contentPadding:
                              const EdgeInsets.symmetric(horizontal: 12),
                        ),
                      ),
                    ),
                    const SizedBox(width: 8),
                    ElevatedButton(
                      onPressed: () {
                        cartProvider.applyPromoCode(_promoCodeController.text);
                        FocusScope.of(context).unfocus(); // Dismiss keyboard
                      },
                      style: ElevatedButton.styleFrom(
                        padding: const EdgeInsets.symmetric(
                            horizontal: 20, vertical: 12),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(8),
                        ),
                      ),
                      child: const Text('Apply'),
                    ),
                  ],
                ),
                if (cartProvider.appliedPromoCode != null)
                  Padding(
                    padding: const EdgeInsets.only(top: 8.0),
                    child: Row(
                      children: [
                        Text(
                          'Promo code "${cartProvider.appliedPromoCode}" applied!',
                          style: const TextStyle(color: Colors.green),
                        ),
                        const SizedBox(width: 8),
                        GestureDetector(
                          onTap: () {
                            cartProvider.removePromoCode();
                            _promoCodeController.clear();
                          },
                          child: const Icon(
                            Icons.close,
                            color: Colors.red,
                            size: 18,
                          ),
                        ),
                      ],
                    ),
                  ),
              ],
            ),
          ),
          const Divider(),

          // Order Summary
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                _buildSummaryRow('Subtotal', '\$${cartProvider.subtotal.toStringAsFixed(2)}'),
                if (cartProvider.discountAmount > 0)
                  _buildSummaryRow(
                    'Discount',
                    '-\$${cartProvider.discountAmount.toStringAsFixed(2)}',
                    textColor: Colors.green,
                  ),
                const Divider(),
                _buildSummaryRow(
                  'Total',
                  '\$${cartProvider.total.toStringAsFixed(2)}',
                  isBold: true,
                  fontSize: 20,
                ),
                const SizedBox(height: 20),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    onPressed: cartProvider.itemCount > 0 ? () => _checkout(context, cartProvider) : null,
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(vertical: 15),
                      backgroundColor: Colors.deepPurple,
                      foregroundColor: Colors.white,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(10),
                      ),
                      textStyle: const TextStyle(fontSize: 18),
                    ),
                    child: const Text('Proceed to Checkout'),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildSummaryRow(String label, String value,
      {Color? textColor, bool isBold = false, double fontSize = 16}) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            label,
            style: TextStyle(
              fontSize: fontSize,
              fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
              color: textColor ?? Colors.black87,
            ),
          ),
          Text(
            value,
            style: TextStyle(
              fontSize: fontSize,
              fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
              color: textColor ?? Colors.black87,
            ),
          ),
        ],
      ),
    );
  }

  void _checkout(BuildContext context, CartProvider cartProvider) {
    // In a real application, you would navigate to a checkout screen,
    // process payment, and then clear the cart upon successful order.
    // For this example, we'll just show a confirmation.
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Checkout Complete!'),
        content: Text(
            'Your order for \$${cartProvider.total.toStringAsFixed(2)} has been placed. Thank you!'),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(ctx).pop();
              cartProvider.clearCart(); // Clear cart after checkout
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Cart cleared successfully!')),
              );
            },
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }
}

5. Individual Cart Item Widget

Each item in the cart will have its own widget to handle quantity changes and removal.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// Assuming CartItem and CartProvider models are defined

class CartItemWidget extends StatelessWidget {
  final CartItem cartItem;

  const CartItemWidget({super.key, required this.cartItem});

  @override
  Widget build(BuildContext context) {
    final cartProvider = 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: const EdgeInsets.only(right: 20),
        margin: const EdgeInsets.symmetric(
          horizontal: 15,
          vertical: 4,
        ),
        child: const Icon(
          Icons.delete,
          color: Colors.white,
          size: 40,
        ),
      ),
      confirmDismiss: (direction) {
        return showDialog(
          context: context,
          builder: (ctx) => AlertDialog(
            title: const Text('Are you sure?'),
            content:
                const Text('Do you want to remove the item from the cart?'),
            actions: [
              TextButton(
                onPressed: () {
                  Navigator.of(ctx).pop(false);
                },
                child: const Text('No'),
              ),
              TextButton(
                onPressed: () {
                  Navigator.of(ctx).pop(true);
                },
                child: const Text('Yes'),
              ),
            ],
          ),
        );
      },
      onDismissed: (direction) {
        cartProvider.removeItem(cartItem.product.id);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('${cartItem.product.name} removed from cart!'),
            duration: const Duration(seconds: 2),
          ),
        );
      },
      child: Card(
        margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 4),
        child: Padding(
          padding: const EdgeInsets.all(8),
          child: ListTile(
            leading: CircleAvatar(
              backgroundImage: NetworkImage(cartItem.product.imageUrl),
              radius: 30,
            ),
            title: Text(cartItem.product.name),
            subtitle: Text(
              'Price: \$${cartItem.product.price.toStringAsFixed(2)}',
            ),
            trailing: Column(
              children: [
                Expanded(
                  child: Text(
                    '\$${cartItem.totalPrice.toStringAsFixed(2)}',
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 16,
                    ),
                  ),
                ),
                Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    InkWell(
                      onTap: () {
                        cartProvider.updateItemQuantity(
                          cartItem.product.id,
                          cartItem.quantity - 1,
                        );
                      },
                      child: const Icon(Icons.remove_circle_outline),
                    ),
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 8.0),
                      child: Text(
                        cartItem.quantity.toString(),
                        style: const TextStyle(fontSize: 16),
                      ),
                    ),
                    InkWell(
                      onTap: () {
                        cartProvider.updateItemQuantity(
                          cartItem.product.id,
                          cartItem.quantity + 1,
                        );
                      },
                      child: const Icon(Icons.add_circle_outline),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

6. Integrating into Your App

Finally, wrap your MaterialApp or the root of your shopping flow with the ChangeNotifierProvider to make your CartProvider accessible throughout the widget tree.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// Assuming all previous classes (Product, CartItem, CartProvider, ShoppingCartScreen) are defined

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (ctx) => CartProvider(),
      child: MaterialApp(
        title: 'Flutter Shopping Cart',
        theme: ThemeData(
          primarySwatch: Colors.deepPurple,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: const ProductListingScreen(), // Your main product listing page
        routes: {
          '/cart': (ctx) => const ShoppingCartScreen(),
        },
      ),
    );
  }
}

// Dummy Product Listing Screen for demonstration
class ProductListingScreen extends StatelessWidget {
  const ProductListingScreen({super.key});

  // Example products
  static final List<Product> _dummyProducts = [
    Product(
      id: 'p1',
      name: 'Designer T-Shirt',
      price: 29.99,
      imageUrl: 'https://via.placeholder.com/150/FF5733/FFFFFF?text=T-Shirt',
    ),
    Product(
      id: 'p2',
      name: 'Wireless Headphones',
      price: 99.99,
      imageUrl: 'https://via.placeholder.com/150/33FF57/FFFFFF?text=Headphones',
    ),
    Product(
      id: 'p3',
      name: 'Smart Watch',
      price: 199.99,
      imageUrl: 'https://via.placeholder.com/150/3357FF/FFFFFF?text=Smart+Watch',
    ),
  ];

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

    return Scaffold(
      appBar: AppBar(
        title: const Text('Products'),
        actions: [
          Consumer<CartProvider>(
            builder: (_, cart, ch) => Badge(
              label: Text(cart.itemCount.toString()),
              child: IconButton(
                icon: const Icon(Icons.shopping_cart),
                onPressed: () {
                  Navigator.of(context).pushNamed('/cart');
                },
              ),
            ),
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: _dummyProducts.length,
        itemBuilder: (ctx, i) {
          final product = _dummyProducts[i];
          return Card(
            margin: const EdgeInsets.all(10),
            child: ListTile(
              leading: Image.network(product.imageUrl, width: 50, height: 50),
              title: Text(product.name),
              subtitle: Text('\$${product.price.toStringAsFixed(2)}'),
              trailing: IconButton(
                icon: const Icon(Icons.add_shopping_cart),
                onPressed: () {
                  cartProvider.addItem(product);
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text('${product.name} added to cart!'),
                      duration: const Duration(seconds: 1),
                    ),
                  );
                },
              ),
            ),
          );
        },
      ),
    );
  }
}

Conclusion

You now have a fully functional shopping cart widget in Flutter with features like editable item quantities, dynamic promo code application, and a clear checkout summary. This foundation can be extended with more advanced features such as persistent storage, complex shipping calculations, and integration with real payment gateways. By leveraging Flutter's reactive UI and the provider package for state management, building such interactive e-commerce components becomes efficient and maintainable.

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