Building a Shopping Cart Widget in Flutter: Featuring Discount Summary, Total Price, and Checkout Functionality

26 Mar 2026

9K

35K

Building a Shopping Cart Widget in Flutter: Featuring Discount Summary, Total Price, and Checkout Functionality

A well-designed shopping cart is a cornerstone of any e-commerce application. It provides users with a clear overview of their selected items, allows them to adjust quantities, apply discounts, and proceed to checkout. This article will guide you through building a robust Flutter shopping cart widget that includes a discount summary, calculates the total price, and features a checkout button.

1. Project Setup and Dependencies

First, create a new Flutter project. We'll use the provider package for state management to manage our cart's state efficiently.

Add the following dependency to your pubspec.yaml file:


dependencies:
  flutter:
    sdk: flutter
  provider: ^6.0.5

Then, run flutter pub get.

2. Data Models

We need simple models for our products and the items within our shopping cart.

product_model.dart


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.dart


import 'package:flutter_app/models/product_model.dart';

class CartItem {
  final Product product;
  int quantity;

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

  // Helper method to get total price for this item
  double get totalPrice => product.price * quantity;
}

3. Cart State Management (CartProvider)

We'll create a CartProvider using ChangeNotifier from the provider package. This class will hold the state of our shopping cart, including the list of items, discount information, and methods to manipulate them.

cart_provider.dart


import 'package:flutter/material.dart';
import 'package:flutter_app/models/cart_item_model.dart';
import 'package:flutter_app/models/product_model.dart';

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

  Map<String, CartItem> get items => {..._items};

  int get itemCount {
    return _items.length;
  }

  // Add an item to the cart
  void addItem(Product product) {
    if (_items.containsKey(product.id)) {
      _items.update(
        product.id,
        (existingCartItem) => CartItem(
          product: existingCartItem.product,
          quantity: existingCartItem.quantity + 1,
        ),
      );
    } else {
      _items.putIfAbsent(
        product.id,
        () => CartItem(
          product: product,
          quantity: 1,
        ),
      );
    }
    notifyListeners();
  }

  // Remove an item entirely from the cart
  void removeItem(String productId) {
    _items.remove(productId);
    notifyListeners();
  }

  // Decrease quantity of an item, remove if quantity becomes 0
  void removeSingleItem(String productId) {
    if (!_items.containsKey(productId)) {
      return;
    }
    if (_items[productId]!.quantity > 1) {
      _items.update(
        productId,
        (existingCartItem) => CartItem(
          product: existingCartItem.product,
          quantity: existingCartItem.quantity - 1,
        ),
      );
    } else {
      _items.remove(productId);
    }
    notifyListeners();
  }

  // Apply a discount code
  void applyDiscount(String code) {
    _promoCode = code;
    // Simple logic: If code is "SAVE10", apply 10% discount
    if (code.toLowerCase() == "save10") {
      _discountPercentage = 0.10; // 10%
    } else if (code.toLowerCase() == "save20") {
      _discountPercentage = 0.20; // 20%
    } else {
      _discountPercentage = 0.0; // No discount
    }
    notifyListeners();
  }

  // Get current promo code
  String get promoCode => _promoCode;

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

  // Calculate discount amount
  double get discountAmount {
    return subtotalAmount * _discountPercentage;
  }

  // Calculate total amount after discount
  double get totalAmount {
    return subtotalAmount - discountAmount;
  }

  void clearCart() {
    _items.clear();
    _discountPercentage = 0.0;
    _promoCode = '';
    notifyListeners();
  }
}

4. Integrating Provider in main.dart

Wrap your root widget (e.g., MaterialApp) with a ChangeNotifierProvider so that the CartProvider instance is available throughout your application.

main.dart


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_app/providers/cart_provider.dart';
import 'package:flutter_app/screens/shopping_cart_screen.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => CartProvider(),
      child: MaterialApp(
        title: 'Flutter Shopping Cart',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const ShoppingCartScreen(),
      ),
    );
  }
}

5. Building the Shopping Cart UI

Now, let's create the UI for our shopping cart. This screen will list the items, provide quantity controls, allow discount application, show a summary, and include a checkout button.

shopping_cart_screen.dart


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_app/models/product_model.dart';
import 'package:flutter_app/providers/cart_provider.dart';

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

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

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

  // Dummy products to add to cart for demonstration
  final List<Product> _dummyProducts = [
    Product(id: 'p1', name: 'Laptop', price: 1200.00, imageUrl: 'https://via.placeholder.com/150'),
    Product(id: 'p2', name: 'Mouse', price: 25.00, imageUrl: 'https://via.placeholder.com/150'),
    Product(id: 'p3', name: 'Keyboard', price: 75.00, imageUrl: 'https://via.placeholder.com/150'),
    Product(id: 'p4', name: 'Monitor', price: 300.00, imageUrl: 'https://via.placeholder.com/150'),
  ];

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

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

    return Scaffold(
      appBar: AppBar(
        title: const Text('Your Cart'),
        actions: [
          IconButton(
            icon: const Icon(Icons.add_shopping_cart),
            onPressed: () {
              // Add some dummy products to cart for testing
              for (var product in _dummyProducts) {
                cart.addItem(product);
              }
            },
          ),
          IconButton(
            icon: const Icon(Icons.delete_forever),
            onPressed: () {
              cart.clearCart();
            },
          ),
        ],
      ),
      body: Column(
        children: <Widget>[
          Expanded(
            child: cartItems.isEmpty
                ? const Center(child: Text('Your cart is empty!'))
                : ListView.builder(
                    itemCount: cartItems.length,
                    itemBuilder: (ctx, i) => CartItemWidget(
                      cartItem: cartItems[i],
                    ),
                  ),
          ),
          _buildDiscountSection(context, cart),
          _buildSummarySection(context, cart),
          _buildCheckoutButton(context, cart),
        ],
      ),
    );
  }

  Widget _buildDiscountSection(BuildContext context, CartProvider cart) {
    return Card(
      margin: const EdgeInsets.all(15),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Apply Discount Code',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _promoCodeController,
                    decoration: InputDecoration(
                      hintText: 'e.g., SAVE10 or SAVE20',
                      suffixIcon: IconButton(
                        icon: const Icon(Icons.clear),
                        onPressed: () {
                          _promoCodeController.clear();
                          cart.applyDiscount(''); // Clear discount
                        },
                      ),
                    ),
                    onChanged: (value) {
                      // Optionally apply discount on change, or wait for button press
                    },
                  ),
                ),
                TextButton(
                  onPressed: () {
                    cart.applyDiscount(_promoCodeController.text);
                    FocusScope.of(context).unfocus(); // Dismiss keyboard
                  },
                  child: const Text('APPLY'),
                ),
              ],
            ),
            if (cart.promoCode.isNotEmpty && cart.discountAmount > 0)
              Padding(
                padding: const EdgeInsets.only(top: 8.0),
                child: Text(
                  'Applied Promo: ${cart.promoCode} (-${(cart.discountAmount / cart.subtotalAmount * 100).toStringAsFixed(0)}%)',
                  style: const TextStyle(color: Colors.green),
                ),
              ),
            if (cart.promoCode.isNotEmpty && cart.discountAmount == 0)
              Padding(
                padding: const EdgeInsets.only(top: 8.0),
                child: Text(
                  'Invalid Promo Code: "${cart.promoCode}"',
                  style: const TextStyle(color: Colors.red),
                ),
              ),
          ],
        ),
      ),
    );
  }

  Widget _buildSummarySection(BuildContext context, CartProvider cart) {
    return Card(
      margin: const EdgeInsets.all(15),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                const Text(
                  'Subtotal',
                  style: TextStyle(fontSize: 18),
                ),
                Text(
                  '\$${cart.subtotalAmount.toStringAsFixed(2)}',
                  style: const TextStyle(fontSize: 18),
                ),
              ],
            ),
            const Divider(),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                const Text(
                  'Discount',
                  style: TextStyle(fontSize: 18, color: Colors.red),
                ),
                Text(
                  '-\$${cart.discountAmount.toStringAsFixed(2)}',
                  style: const TextStyle(fontSize: 18, color: Colors.red),
                ),
              ],
            ),
            const Divider(),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                const Text(
                  'Total',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                ),
                Text(
                  '\$${cart.totalAmount.toStringAsFixed(2)}',
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Theme.of(context).primaryColor,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildCheckoutButton(BuildContext context, CartProvider cart) {
    return Padding(
      padding: const EdgeInsets.all(15.0),
      child: ElevatedButton(
        onPressed: cart.totalAmount <= 0
            ? null // Disable button if cart is empty or total is 0
            : () {
                // Implement your checkout logic here
                showDialog(
                  context: context,
                  builder: (ctx) => AlertDialog(
                    title: const Text('Checkout Complete!'),
                    content: Text('Your total order: \$${cart.totalAmount.toStringAsFixed(2)}'),
                    actions: <Widget>[
                      TextButton(
                        child: const Text('Okay'),
                        onPressed: () {
                          Navigator.of(ctx).pop();
                          cart.clearCart(); // Clear cart after successful checkout
                        },
                      ),
                    ],
                  ),
                );
              },
        style: ElevatedButton.styleFrom(
          minimumSize: const Size.fromHeight(50), // Make button full width
          backgroundColor: Theme.of(context).primaryColor,
        ),
        child: const Text(
          'CHECKOUT',
          style: TextStyle(fontSize: 18, color: Colors.white),
        ),
      ),
    );
  }
}

// Widget for displaying individual cart items
class CartItemWidget extends StatelessWidget {
  final CartItem cartItem;

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

  @override
  Widget build(BuildContext context) {
    final cart = 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: <Widget>[
              TextButton(
                child: const Text('No'),
                onPressed: () {
                  Navigator.of(ctx).pop(false);
                },
              ),
              TextButton(
                child: const Text('Yes'),
                onPressed: () {
                  Navigator.of(ctx).pop(true);
                },
              ),
            ],
          ),
        );
      },
      onDismissed: (direction) {
        cart.removeItem(cartItem.product.id);
      },
      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(
              'Total: \$${cartItem.totalPrice.toStringAsFixed(2)}',
            ),
            trailing: Row(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                IconButton(
                  icon: const Icon(Icons.remove),
                  onPressed: () {
                    cart.removeSingleItem(cartItem.product.id);
                  },
                ),
                Text('${cartItem.quantity}x'),
                IconButton(
                  icon: const Icon(Icons.add),
                  onPressed: () {
                    cart.addItem(cartItem.product);
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Conclusion

In this article, we've walked through the process of building a comprehensive shopping cart widget in Flutter. We leveraged the provider package for efficient state management, created robust data models for products and cart items, and implemented a user-friendly UI. Key features include dynamic quantity adjustments, discount code application with a summary, and a clear display of subtotal, discount, and total price, culminating in a functional checkout button. This foundation can be extended further with persistent storage, complex discount rules, and integration with actual payment gateways.

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