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

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

Aug 02, 2026

Building a Flutter Notification Center with Grouping & Actions

Learn to create a sophisticated in-app notification center in Flutter, featuring grouping, swipe-to-dismiss, custom icons, and actionable buttons for enhanced u

Aug 01, 2026

Flutter Layout Tips: Using FractionallySizedBox, AspectRatio, ConstrainedBox for Adaptive UI

Master Flutter's FractionallySizedBox, AspectRatio, and ConstrainedBox to build truly adaptive UIs. Learn how to control widget sizing, maintain proportions, an

Aug 01, 2026

Flutter Product Quick View: Animation, Related Items, Badges

Learn to create a dynamic product quick view widget in Flutter, incorporating smooth animations, relevant related items, and eye-catching promo badges for an en