Building a Shopping Cart Widget with Discount, Summary, and Checkout in Flutter

12 Mar 2026

9K

35K

Building a Shopping Cart Widget with Discount, Summary, and Checkout in Flutter

A robust shopping cart is a fundamental component of any e-commerce application. It allows users to manage selected products, review prices, apply discounts, and proceed to checkout. Building a feature-rich shopping cart in Flutter provides a highly customizable and performant solution. This article will guide you through creating a dynamic shopping cart widget, incorporating discount application, a clear summary, and a functional checkout process.

1. Defining the Data Models

First, let's establish the necessary data models for our products and cart items. We'll also define a simple discount model.

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

Each item in the cart will reference a Product and track its quantity.


class CartItem {
  final Product product;
  int quantity;

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

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

Discount Model

A simple model for applying discounts, either by percentage or a fixed amount.


enum DiscountType { percentage, fixed }

class Discount {
  final String code;
  final double value; // Percentage (e.g., 0.10 for 10%) or fixed amount
  final DiscountType type;

  Discount({
    required this.code,
    required this.value,
    required this.type,
  });
}

2. State Management with Provider

For managing the cart's state, we'll use the provider package. This CartProvider class will hold the cart items, apply discounts, and calculate totals.


import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart'; // For firstWhereOrNull

class CartProvider with ChangeNotifier {
  final List<CartItem> _items = [];
  Discount? _appliedDiscount;

  List<CartItem> get items => [..._items]; // Immutable copy

  Discount? get appliedDiscount => _appliedDiscount;

  // Getters for calculations
  double get subtotal {
    return _items.fold(0.0, (sum, item) => sum + item.totalPrice);
  }

  double get discountAmount {
    if (_appliedDiscount == null) return 0.0;

    if (_appliedDiscount!.type == DiscountType.percentage) {
      return subtotal * _appliedDiscount!.value;
    } else {
      return _appliedDiscount!.value; // Fixed amount
    }
  }

  double get total => subtotal - discountAmount;

  // Cart actions
  void addItem(Product product) {
    final existingItem = _items.firstWhereOrNull(
      (item) => item.product.id == product.id,
    );
    if (existingItem != null) {
      existingItem.quantity++;
    } else {
      _items.add(CartItem(product: product));
    }
    notifyListeners();
  }

  void removeItem(String productId) {
    _items.removeWhere((item) => item.product.id == productId);
    notifyListeners();
  }

  void updateItemQuantity(String productId, int newQuantity) {
    final existingItem = _items.firstWhereOrNull(
      (item) => item.product.id == productId,
    );
    if (existingItem != null) {
      if (newQuantity > 0) {
        existingItem.quantity = newQuantity;
      } else {
        _items.removeWhere((item) => item.product.id == productId);
      }
      notifyListeners();
    }
  }

  void applyDiscount(String discountCode) {
    // In a real app, you would fetch this from a backend.
    // For this example, we'll use a hardcoded list.
    final List<Discount> availableDiscounts = [
      Discount(code: 'SAVE10', value: 0.10, type: DiscountType.percentage),
      Discount(code: 'FLAT5', value: 5.0, type: DiscountType.fixed),
    ];

    final discount = availableDiscounts.firstWhereOrNull(
      (d) => d.code.toLowerCase() == discountCode.toLowerCase(),
    );

    if (discount != null) {
      _appliedDiscount = discount;
      print('Discount ${discount.code} applied!');
    } else {
      _appliedDiscount = null; // Clear previous discount if invalid
      print('Invalid discount code.');
    }
    notifyListeners();
  }

  void removeDiscount() {
    _appliedDiscount = null;
    notifyListeners();
  }

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

3. Building the Cart Item Widget

Each item in the shopping cart will be represented by a CartItemWidget. This widget displays product details and allows users to modify quantities or remove the item.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Assume your models and provider are imported here

class CartItemWidget extends StatelessWidget {
  final CartItem cartItem;

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

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

    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Row(
          children: [
            SizedBox(
              width: 80,
              height: 80,
              child: Image.network(
                cartItem.product.imageUrl,
                fit: BoxFit.cover,
              ),
            ),
            const SizedBox(width: 10),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    cartItem.product.name,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  Text('\$${cartItem.product.price.toStringAsFixed(2)}'),
                  Row(
                    children: [
                      IconButton(
                        icon: const Icon(Icons.remove_circle_outline),
                        onPressed: () {
                          cart.updateItemQuantity(
                            cartItem.product.id,
                            cartItem.quantity - 1,
                          );
                        },
                      ),
                      Text('${cartItem.quantity}'),
                      IconButton(
                        icon: const Icon(Icons.add_circle_outline),
                        onPressed: () {
                          cart.updateItemQuantity(
                            cartItem.product.id,
                            cartItem.quantity + 1,
                          );
                        },
                      ),
                      const Spacer(),
                      Text(
                        '\$${cartItem.totalPrice.toStringAsFixed(2)}',
                        style: const TextStyle(fontWeight: FontWeight.bold),
                      ),
                    ],
                  ),
                ],
              ),
            ),
            IconButton(
              icon: const Icon(Icons.delete, color: Colors.red),
              onPressed: () {
                cart.removeItem(cartItem.product.id);
              },
            ),
          ],
        ),
      ),
    );
  }
}

4. Implementing Discount Logic and Input

We'll create a section where users can input a discount code and apply it. The CartProvider handles the actual logic and calculation.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Assume CartProvider is imported

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

  @override
  State<DiscountInputWidget> createState() => _DiscountInputWidgetState();
}

class _DiscountInputWidgetState extends State<DiscountInputWidget> {
  final TextEditingController _discountController = TextEditingController();

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

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

    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            'Have a discount code?',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: TextField(
                  controller: _discountController,
                  decoration: InputDecoration(
                    hintText: 'Enter discount code',
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(8),
                    ),
                    contentPadding:
                        const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
                  ),
                ),
              ),
              const SizedBox(width: 10),
              ElevatedButton(
                onPressed: () {
                  cart.applyDiscount(_discountController.text);
                },
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 12),
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(8),
                  ),
                ),
                child: const Text('Apply'),
              ),
            ],
          ),
          if (cart.appliedDiscount != null)
            Padding(
              padding: const EdgeInsets.only(top: 8.0),
              child: Row(
                children: [
                  Text(
                    'Applied: ${cart.appliedDiscount!.code}',
                    style: const TextStyle(color: Colors.green),
                  ),
                  const Spacer(),
                  TextButton(
                    onPressed: () {
                      cart.removeDiscount();
                      _discountController.clear();
                    },
                    child: const Text('Remove', style: TextStyle(color: Colors.red)),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
}

5. Creating the Cart Summary Widget

The cart summary displays the breakdown of costs: subtotal, discount, and the final total. This widget will listen to changes in the CartProvider to update automatically.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Assume CartProvider is imported

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

  @override
  Widget build(BuildContext context) {
    return Consumer<CartProvider>(
      builder: (context, cart, 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 Divider(),
                _buildSummaryRow('Subtotal:', cart.subtotal),
                if (cart.appliedDiscount != null)
                  _buildSummaryRow(
                    'Discount (${cart.appliedDiscount!.code}):',
                    -cart.discountAmount, // Show as negative for discount
                    color: Colors.green,
                  ),
                const Divider(),
                _buildSummaryRow(
                  'Total:',
                  cart.total,
                  isBold: true,
                  fontSize: 18,
                ),
              ],
            ),
          ),
        );
      },
    );
  }

  Widget _buildSummaryRow(String label, double amount,
      {Color? color, 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: color,
            ),
          ),
          Text(
            '\$${amount.toStringAsFixed(2)}',
            style: TextStyle(
              fontSize: fontSize,
              fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
              color: color,
            ),
          ),
        ],
      ),
    );
  }
}

6. Developing the Checkout Process

The checkout button will trigger a simple action. In a real application, this would lead to payment processing, address selection, etc. For this example, we'll just show a confirmation message and clear the cart.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Assume CartProvider is imported

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

  void _performCheckout(BuildContext context) {
    final cart = Provider.of<CartProvider>(context, listen: false);
    if (cart.items.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Your cart is empty!')),
      );
      return;
    }

    // Simulate a checkout process
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Order Placed!'),
        content: Text(
            'Your order for \$${cart.total.toStringAsFixed(2)} has been placed successfully.'),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(ctx).pop();
              cart.clearCart(); // Clear the cart after successful checkout
            },
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: SizedBox(
        width: double.infinity,
        child: ElevatedButton(
          onPressed: () => _performCheckout(context),
          style: ElevatedButton.styleFrom(
            padding: const EdgeInsets.symmetric(vertical: 16),
            textStyle: const TextStyle(fontSize: 18),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(10),
            ),
          ),
          child: const Text('Proceed to Checkout'),
        ),
      ),
    );
  }
}

7. Assembling the Main Cart Screen

Now, let's put all these components together into a complete CartScreen.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Assume all models, CartProvider, CartItemWidget, DiscountInputWidget,
// CartSummaryWidget, and CheckoutButton are imported.

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Your Shopping Cart'),
      ),
      body: Consumer<CartProvider>(
        builder: (context, cart, child) {
          if (cart.items.isEmpty) {
            return const Center(
              child: Text(
                'Your cart is empty!',
                style: TextStyle(fontSize: 18, color: Colors.grey),
              ),
            );
          }
          return Column(
            children: [
              Expanded(
                child: ListView.builder(
                  itemCount: cart.items.length,
                  itemBuilder: (context, index) {
                    return CartItemWidget(cartItem: cart.items[index]);
                  },
                ),
              ),
              const DiscountInputWidget(),
              const CartSummaryWidget(),
              const CheckoutButton(),
            ],
          );
        },
      ),
    );
  }
}

Integrating into your Flutter App

To use this, wrap your app or a relevant part of it with a ChangeNotifierProvider:


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Assume CartProvider and CartScreen are imported

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

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

  @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: const ProductListingScreen(), // Or whichever screen leads to your cart
      ),
    );
  }
}

// Example ProductListingScreen to add items to cart
class ProductListingScreen extends StatelessWidget {
  const ProductListingScreen({super.key});

  final List<Product> sampleProducts = const [
    Product(id: 'p1', name: 'Laptop', price: 1200.00, imageUrl: 'https://via.placeholder.com/150/FF0000/FFFFFF?text=Laptop'),
    Product(id: 'p2', name: 'Mouse', price: 25.00, imageUrl: 'https://via.placeholder.com/150/0000FF/FFFFFF?text=Mouse'),
    Product(id: 'p3', name: 'Keyboard', price: 75.00, imageUrl: 'https://via.placeholder.com/150/00FF00/FFFFFF?text=Keyboard'),
  ];

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

Conclusion

You have successfully built a functional shopping cart widget in Flutter, complete with item management, discount application, a clear order summary, and a basic checkout flow. By utilizing provider for state management, the application remains organized and responsive to user interactions.

Further Enhancements:

  • **Persistent Storage:** Save cart items locally using shared_preferences or a database.
  • **Backend Integration:** Fetch products, validate discounts, and process orders through a backend API.
  • **Animations:** Add subtle animations for item additions or removals.
  • **Error Handling:** Implement robust error handling for discount codes and checkout failures.
  • **Payment Gateway:** Integrate with actual payment gateways (e.g., Stripe, PayPal).

This foundation can be extended and customized to fit the specific needs of any e-commerce application.

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