image

03 Apr 2026

9K

35K

Building a Shopping Cart Widget in Flutter with Discount Summary, Total Price, Checkout, and Promo Banner

In modern e-commerce applications, a well-designed and functional shopping cart is crucial for a seamless user experience. It's the central hub where users review their selected items, apply discounts, and proceed to purchase. This article will guide you through creating a robust shopping cart widget in Flutter, incorporating essential features like a dynamic discount summary, real-time total price calculation, a checkout mechanism, and an engaging promo banner.

Core Components of Our Shopping Cart Widget

To build a comprehensive shopping cart, we'll focus on integrating the following key features:

  • Item Listing: Displaying all products added to the cart, with options to adjust quantity or remove items.
  • Discount Summary: Clearly showing applied discounts, either from promo codes or other promotions.
  • Total Price Calculation: Dynamically updating the subtotal, discount, and final total price.
  • Promo Banner/Input: A section for users to discover or apply promotional codes.
  • Checkout Button: The gateway to finalizing the purchase.

Setting Up Your Flutter Project

First, ensure you have Flutter installed and set up a new project. We'll use the provider package for state management, which simplifies managing the cart's state across different widgets.

Add the provider dependency to your pubspec.yaml:


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

Then, run flutter pub get.

1. Data Models for Cart Items and Promo Codes

We'll define simple data models for a CartItem and a PromoCode. These models will hold the necessary information for each item in the cart and any active promotions.

cart_item.dart


class CartItem {
  final String id;
  final String name;
  final double price;
  int quantity;
  final String imageUrl;

  CartItem({
    required this.id,
    required this.name,
    required this.price,
    this.quantity = 1,
    required this.imageUrl,
  });

  CartItem copyWith({
    String? id,
    String? name,
    double? price,
    int? quantity,
    String? imageUrl,
  }) {
    return CartItem(
      id: id ?? this.id,
      name: name ?? this.name,
      price: price ?? this.price,
      quantity: quantity ?? this.quantity,
      imageUrl: imageUrl ?? this.imageUrl,
    );
  }
}

promo_code.dart


class PromoCode {
  final String code;
  final double discountPercentage; // e.g., 0.10 for 10%
  final double minOrderAmount;

  PromoCode({
    required this.code,
    required this.discountPercentage,
    this.minOrderAmount = 0.0,
  });

  bool isValid(double currentSubtotal) {
    return currentSubtotal >= minOrderAmount;
  }
}

2. The Shopping Cart Provider (State Management)

The CartProvider will manage the state of our shopping cart, including adding/removing items, updating quantities, applying promo codes, and calculating totals. We'll extend ChangeNotifier and use notifyListeners() to update the UI.


import 'package:flutter/material.dart';
import 'package:your_app_name/models/cart_item.dart';
import 'package:your_app_name/models/promo_code.dart'; // Adjust import path

class CartProvider extends ChangeNotifier {
  final Map _items = {};
  PromoCode? _activePromoCode;

  Map get items => {..._items}; // Return a copy
  PromoCode? get activePromoCode => _activePromoCode;

  int get itemCount => _items.length;

  double get subtotal {
    double total = 0.0;
    _items.forEach((key, item) {
      total += item.price * item.quantity;
    });
    return total;
  }

  double get discountAmount {
    if (_activePromoCode != null && _activePromoCode!.isValid(subtotal)) {
      return subtotal * _activePromoCode!.discountPercentage;
    }
    return 0.0;
  }

  double get totalAmount {
    return subtotal - discountAmount;
  }

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

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

  void increaseQuantity(String productId) {
    if (_items.containsKey(productId)) {
      _items.update(
        productId,
        (existingItem) => existingItem.copyWith(quantity: existingItem.quantity + 1),
      );
      notifyListeners();
    }
  }

  void decreaseQuantity(String productId) {
    if (_items.containsKey(productId)) {
      if (_items[productId]!.quantity > 1) {
        _items.update(
          productId,
          (existingItem) => existingItem.copyWith(quantity: existingItem.quantity - 1),
        );
      } else {
        _items.remove(productId); // Remove if quantity drops to 0
      }
      notifyListeners();
    }
  }

  void applyPromoCode(PromoCode promo) {
    _activePromoCode = promo;
    notifyListeners();
  }

  void removePromoCode() {
    _activePromoCode = null;
    notifyListeners();
  }

  void clearCart() {
    _items.clear();
    _activePromoCode = null;
    notifyListeners();
  }
}

You'll need to wrap your MaterialApp or the relevant part of your widget tree with a ChangeNotifierProvider:


// In main.dart or your root widget
import 'package:provider/provider.dart';
import 'package:your_app_name/providers/cart_provider.dart'; // Adjust import path

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartProvider(),
      child: MyApp(),
    ),
  );
}

3. Implementing the Cart Item Widget

Each item in the cart will have its own widget to display details and allow quantity adjustments.


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

class CartItemWidget extends StatelessWidget {
  final CartItem cartItem;

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

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

    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Row(
          children: [
            Container(
              width: 80,
              height: 80,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(8),
                image: DecorationImage(
                  image: NetworkImage(cartItem.imageUrl),
                  fit: BoxFit.cover,
                ),
              ),
            ),
            const SizedBox(width: 10),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    cartItem.name,
                    style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  Text('\$${cartItem.price.toStringAsFixed(2)}',
                      style: const TextStyle(fontSize: 14, color: Colors.grey)),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Row(
                        children: [
                          IconButton(
                            icon: const Icon(Icons.remove_circle_outline),
                            onPressed: () {
                              cartProvider.decreaseQuantity(cartItem.id);
                            },
                          ),
                          Text('${cartItem.quantity}', style: const TextStyle(fontSize: 16)),
                          IconButton(
                            icon: const Icon(Icons.add_circle_outline),
                            onPressed: () {
                              cartProvider.increaseQuantity(cartItem.id);
                            },
                          ),
                        ],
                      ),
                      Text(
                        '\$${(cartItem.price * cartItem.quantity).toStringAsFixed(2)}',
                        style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

4. Building the Discount Summary and Total Price Section

This section will display the subtotal, the applied discount (if any), and the final total amount. It will react to changes in the cart or promo code application.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app_name/providers/cart_provider.dart';

class CartSummaryWidget extends StatelessWidget {
  const CartSummaryWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, cartProvider, child) {
        return Card(
          margin: const EdgeInsets.all(16),
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  'Order Summary',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 10),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    const Text('Subtotal:', style: TextStyle(fontSize: 16)),
                    Text('\$${cartProvider.subtotal.toStringAsFixed(2)}',
                        style: const TextStyle(fontSize: 16)),
                  ],
                ),
                const SizedBox(height: 5),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    const Text('Discount:', style: TextStyle(fontSize: 16)),
                    Text('-\$${cartProvider.discountAmount.toStringAsFixed(2)}',
                        style: const TextStyle(fontSize: 16, color: Colors.green)),
                  ],
                ),
                if (cartProvider.activePromoCode != null)
                  Padding(
                    padding: const EdgeInsets.only(top: 4.0),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text(
                          'Promo (${cartProvider.activePromoCode!.code}):',
                          style: const TextStyle(fontSize: 14, color: Colors.green),
                        ),
                        GestureDetector(
                          onTap: () {
                            cartProvider.removePromoCode();
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(content: Text('Promo code removed.')),
                            );
                          },
                          child: const Text(
                            'Remove',
                            style: TextStyle(fontSize: 14, color: Colors.red, decoration: TextDecoration.underline),
                          ),
                        ),
                      ],
                    ),
                  ),
                const Divider(height: 20, thickness: 1),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    const Text('Total:',
                        style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                    Text('\$${cartProvider.totalAmount.toStringAsFixed(2)}',
                        style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                  ],
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

5. Integrating the Promo Banner

This widget will allow users to input a promo code. For demonstration, we'll have a predefined set of codes.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app_name/models/promo_code.dart';
import 'package:your_app_name/providers/cart_provider.dart';

class PromoBannerWidget extends StatefulWidget {
  const PromoBannerWidget({Key? key}) : super(key: key);

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

class _PromoBannerWidgetState extends State {
  final TextEditingController _promoCodeController = TextEditingController();
  final List availablePromoCodes = [
    PromoCode(code: 'FLUTTER20', discountPercentage: 0.20, minOrderAmount: 50.0), // 20% off
    PromoCode(code: 'SAVE10', discountPercentage: 0.10, minOrderAmount: 20.0), // 10% off
  ];

  void _applyPromo(BuildContext context) {
    final String enteredCode = _promoCodeController.text.trim().toUpperCase();
    final cartProvider = Provider.of(context, listen: false);

    if (enteredCode.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Please enter a promo code.')),
      );
      return;
    }

    final PromoCode? foundPromo = availablePromoCodes.firstWhere(
      (promo) => promo.code == enteredCode,
      orElse: () => PromoCode(code: '', discountPercentage: 0.0), // Dummy if not found
    );

    if (foundPromo != null && foundPromo.code.isNotEmpty) {
      if (foundPromo.isValid(cartProvider.subtotal)) {
        cartProvider.applyPromoCode(foundPromo);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Promo code "${foundPromo.code}" applied!')),
        );
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Order subtotal must be \$${foundPromo.minOrderAmount.toStringAsFixed(2)} to use this promo.')),
        );
      }
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Invalid promo code: "$enteredCode".')),
      );
    }
    _promoCodeController.clear();
  }

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

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, cartProvider, child) {
        if (cartProvider.activePromoCode != null) {
          return Container(
            margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.green.withOpacity(0.1),
              borderRadius: BorderRadius.circular(8),
              border: Border.all(color: Colors.green),
            ),
            child: Row(
              children: [
                const Icon(Icons.check_circle_outline, color: Colors.green),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(
                    'Promo code "${cartProvider.activePromoCode!.code}" applied!',
                    style: const TextStyle(color: Colors.green, fontWeight: FontWeight.bold),
                  ),
                ),
                GestureDetector(
                  onTap: () {
                    cartProvider.removePromoCode();
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('Promo code removed.')),
                    );
                  },
                  child: const Text(
                    'Remove',
                    style: TextStyle(color: Colors.red, decoration: TextDecoration.underline),
                  ),
                ),
              ],
            ),
          );
        }

        return Card(
          margin: const EdgeInsets.all(16),
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  'Have a Promo Code?',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 10),
                TextField(
                  controller: _promoCodeController,
                  decoration: InputDecoration(
                    hintText: 'Enter code here',
                    border: const OutlineInputBorder(),
                    suffixIcon: IconButton(
                      icon: const Icon(Icons.arrow_forward),
                      onPressed: () => _applyPromo(context),
                    ),
                  ),
                  onSubmitted: (_) => _applyPromo(context),
                ),
                const SizedBox(height: 10),
                const Text(
                  'Available codes: FLUTTER20 (min \$50), SAVE10 (min \$20)',
                  style: TextStyle(fontSize: 12, color: Colors.grey),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

6. The Checkout Button

A simple button that initiates the checkout process. For this article, we'll just show a confirmation message.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app_name/providers/cart_provider.dart';

class CheckoutButtonWidget extends StatelessWidget {
  const CheckoutButtonWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, cartProvider, child) {
        return Container(
          width: double.infinity,
          padding: const EdgeInsets.all(16),
          child: ElevatedButton(
            onPressed: cartProvider.itemCount > 0
                ? () {
                    // Implement your checkout logic here
                    // e.g., navigate to payment screen, submit order
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('Proceeding to checkout for \$${cartProvider.totalAmount.toStringAsFixed(2)}')),
                    );
                    cartProvider.clearCart(); // Clear cart after "checkout"
                  }
                : null, // Disable button if cart is empty
            style: ElevatedButton.styleFrom(
              padding: const EdgeInsets.symmetric(vertical: 15),
              backgroundColor: Colors.blueAccent, // Use primary color for checkout button
              foregroundColor: Colors.white,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(10),
              ),
            ),
            child: Text(
              'Checkout (\$${cartProvider.totalAmount.toStringAsFixed(2)})',
              style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
          ),
        );
      },
    );
  }
}

7. Putting It All Together: The Main Cart Screen

Now, let's assemble all these widgets into a complete ShoppingCartScreen.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app_name/models/cart_item.dart';
import 'package:your_app_name/providers/cart_provider.dart';
import 'package:your_app_name/widgets/cart_item_widget.dart';
import 'package:your_app_name/widgets/cart_summary_widget.dart';
import 'package:your_app_name/widgets/promo_banner_widget.dart';
import 'package:your_app_name/widgets/checkout_button_widget.dart';

class ShoppingCartScreen extends StatefulWidget {
  const ShoppingCartScreen({Key? key}) : super(key: key);

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

class _ShoppingCartScreenState extends State {
  @override
  void initState() {
    super.initState();
    // Add some dummy items to the cart for demonstration
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final cartProvider = Provider.of(context, listen: false);
      if (cartProvider.itemCount == 0) {
        cartProvider.addItem(CartItem(
            id: 'p1',
            name: 'Stylish T-Shirt',
            price: 25.00,
            imageUrl: 'https://picsum.photos/id/1018/200/200'));
        cartProvider.addItem(CartItem(
            id: 'p2',
            name: 'Designer Jeans',
            price: 60.00,
            imageUrl: 'https://picsum.photos/id/1025/200/200'));
        cartProvider.addItem(CartItem(
            id: 'p3',
            name: 'Running Shoes',
            price: 80.00,
            imageUrl: 'https://picsum.photos/id/1084/200/200'));
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Your Shopping Cart'),
      ),
      body: Consumer(
        builder: (context, cartProvider, child) {
          if (cartProvider.itemCount == 0) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Icon(Icons.shopping_cart_outlined, size: 80, color: Colors.grey),
                  const SizedBox(height: 10),
                  const Text(
                    'Your cart is empty!',
                    style: TextStyle(fontSize: 20, color: Colors.grey),
                  ),
                  const SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: () {
                      // Navigate back to shopping or add some sample items
                      Navigator.of(context).pop();
                      // For demo, re-add items:
                      Provider.of(context, listen: false).addItem(CartItem(
                          id: 'p1',
                          name: 'Stylish T-Shirt',
                          price: 25.00,
                          imageUrl: 'https://picsum.photos/id/1018/200/200'));
                      Provider.of(context, listen: false).addItem(CartItem(
                          id: 'p2',
                          name: 'Designer Jeans',
                          price: 60.00,
                          imageUrl: 'https://picsum.photos/id/1025/200/200'));
                    },
                    child: const Text('Continue Shopping (or Add Samples)'),
                  ),
                ],
              ),
            );
          }
          return Column(
            children: [
              Expanded(
                child: ListView.builder(
                  itemCount: cartProvider.items.length,
                  itemBuilder: (context, index) {
                    final cartItem = cartProvider.items.values.toList()[index];
                    return CartItemWidget(cartItem: cartItem);
                  },
                ),
              ),
              const PromoBannerWidget(),
              const CartSummaryWidget(),
              const CheckoutButtonWidget(),
            ],
          );
        },
      ),
    );
  }
}

You can then navigate to ShoppingCartScreen from your main application wherever appropriate.


// Example of navigating to the cart screen
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const ShoppingCartScreen()));

Conclusion

By following this guide, you've successfully created a feature-rich shopping cart widget in Flutter. We leveraged the provider package for efficient state management, designed clear data models, and implemented modular widgets for each component: displaying cart items, applying and summarizing discounts, managing promo codes, and providing a checkout interface. This robust foundation can be further extended with animations, database integration, payment gateway implementation, and more complex discount rules to build a complete e-commerce experience.

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