image

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

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