Building a Shopping Cart Summary Widget with Checkout Button in Flutter
Introduction
A shopping cart summary is a critical component in any e-commerce application. It provides users with a clear overview of the items they intend to purchase, including quantities, individual prices, and the calculated total. Integrating a prominent checkout button ensures a smooth transition from browsing to completing a transaction. This article will guide you through building a professional Flutter widget for a shopping cart summary, complete with a functional checkout button, using best practices for state management.
Prerequisites
Before diving in, ensure you have a basic understanding of Flutter development, including widgets, state management concepts (we'll be using the provider package for simplicity), and asynchronous operations.
1. Setting Up the Data Models and State Management
First, we need to define the data structures for our products and cart items. We'll also set up a state management solution to manage the cart's state across the application.
Product Model
A simple Product class to represent items available for purchase.
class Product {
final String id;
final String name;
final double price;
Product({
required this.id,
required this.name,
required this.price,
});
}
CartItem Model
The CartItem class combines a Product with its chosen quantity.
class CartItem {
final Product product;
int quantity;
CartItem({
required this.product,
this.quantity = 1,
});
double get totalPrice => product.price * quantity;
}
CartProvider (State Management with Provider)
We'll use a ChangeNotifier from the provider package to manage the cart's state. This class will hold the list of CartItems and provide methods to manipulate the cart.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; // Required for @required, although no longer strictly needed for ChangeNotifier
class CartProvider with ChangeNotifier {
final List<CartItem> _items = [];
List<CartItem> get items => [..._items]; // Return a copy to prevent external modification
int get itemCount => _items.length;
double get subtotal {
double total = 0.0;
for (var item in _items) {
total += item.totalPrice;
}
return total;
}
double get taxRate => 0.08; // Example tax rate (8%)
double get taxAmount => subtotal * taxRate;
double get totalAmount => subtotal + taxAmount;
void addItem(Product product) {
int index = _items.indexWhere((item) => item.product.id == product.id);
if (index >= 0) {
_items[index].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) {
int index = _items.indexWhere((item) => item.product.id == productId);
if (index >= 0) {
if (newQuantity > 0) {
_items[index].quantity = newQuantity;
} else {
_items.removeAt(index); // Remove if quantity becomes 0 or less
}
notifyListeners();
}
}
void clearCart() {
_items.clear();
notifyListeners();
}
}
2. Designing the Cart Summary Widget
Now, let's build the UI for our cart summary. This widget will display each item, calculate totals, and house the checkout button.
CartItemTile Widget
A dedicated widget to display a single cart item. This helps keep the main summary widget clean.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app_name/models/cart_item.dart'; // Adjust import paths as needed
import 'package:your_app_name/providers/cart_provider.dart'; // Adjust import paths as needed
class CartItemTile extends StatelessWidget {
final CartItem cartItem;
const CartItemTile({Key? key, required this.cartItem}) : super(key: key);
@override
Widget build(BuildContext context) {
final cartProvider = Provider.of<CartProvider>(context, listen: false);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 4),
child: Padding(
padding: const EdgeInsets.all(8),
child: ListTile(
leading: CircleAvatar(
child: Padding(
padding: const EdgeInsets.all(5),
child: FittedBox(
child: Text('\$${cartItem.product.price.toStringAsFixed(2)}'),
),
),
),
title: Text(cartItem.product.name),
subtitle: Text('Total: \$${cartItem.totalPrice.toStringAsFixed(2)}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: () {
cartProvider.updateItemQuantity(cartItem.product.id, cartItem.quantity - 1);
},
),
Text('${cartItem.quantity}x'),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
cartProvider.updateItemQuantity(cartItem.product.id, cartItem.quantity + 1);
},
),
IconButton(
icon: const Icon(Icons.delete),
color: Theme.of(context).errorColor,
onPressed: () {
cartProvider.removeItem(cartItem.product.id);
},
),
],
),
),
),
);
}
}
The Main CartSummaryWidget
This widget will consume the CartProvider and display the list of items, summary totals, and the checkout button.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app_name/providers/cart_provider.dart'; // Adjust import paths as needed
import 'package:your_app_name/widgets/cart_item_tile.dart'; // Adjust import paths as needed
import 'package:your_app_name/screens/checkout_screen.dart'; // Adjust import paths as needed
class CartSummaryWidget extends StatelessWidget {
const CartSummaryWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<CartProvider>(
builder: (context, cartProvider, child) {
return Column(
children: [
Expanded(
child: cartProvider.items.isEmpty
? Center(
child: Text(
'Your cart is empty!',
style: Theme.of(context).textTheme.headline6,
),
)
: ListView.builder(
itemCount: cartProvider.itemCount,
itemBuilder: (ctx, i) => CartItemTile(
cartItem: cartProvider.items[i],
),
),
),
if (cartProvider.items.isNotEmpty)
Padding(
padding: const EdgeInsets.all(15),
child: Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildSummaryRow(
context,
'Subtotal:',
'\$${cartProvider.subtotal.toStringAsFixed(2)}',
),
const SizedBox(height: 8),
_buildSummaryRow(
context,
'Tax (${(cartProvider.taxRate * 100).toStringAsFixed(0)}%):',
'\$${cartProvider.taxAmount.toStringAsFixed(2)}',
),
const Divider(),
_buildSummaryRow(
context,
'Total:',
'\$${cartProvider.totalAmount.toStringAsFixed(2)}',
isBold: true,
textSize: 20,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: cartProvider.items.isEmpty
? null
: () {
// Perform checkout logic
_performCheckout(context, cartProvider);
},
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50), // Full width button
),
child: const Text(
'Checkout',
style: TextStyle(fontSize: 18),
),
),
],
),
),
),
),
SizedBox(height: cartProvider.items.isNotEmpty ? 10 : 0), // Small spacing if cart is not empty
],
);
},
);
}
Widget _buildSummaryRow(BuildContext context, String title, String value,
{bool isBold = false, double textSize = 16}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(
fontSize: textSize,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
),
),
Text(
value,
style: TextStyle(
fontSize: textSize,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
color: isBold ? Theme.of(context).primaryColor : null,
),
),
],
);
}
void _performCheckout(BuildContext context, CartProvider cartProvider) async {
// Example: Simulate an asynchronous order placement
// In a real app, this would involve API calls, payment processing, etc.
final bool checkoutSuccess = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Confirm Checkout'),
content: Text(
'Are you sure you want to proceed with the payment of \$${cartProvider.totalAmount.toStringAsFixed(2)}?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(ctx).pop(false); // User cancelled
},
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
Navigator.of(ctx).pop(true); // User confirmed
},
child: const Text('Confirm'),
),
],
),
) ?? false; // Default to false if dialog is dismissed
if (checkoutSuccess) {
// Clear the cart after successful checkout
cartProvider.clearCart();
// Navigate to a checkout confirmation screen
Navigator.of(context).push(
MaterialPageRoute(
builder: (ctx) => CheckoutScreen(
orderAmount: cartProvider.totalAmount,
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Checkout cancelled.')),
);
}
}
}
3. Integrating the Checkout Button Logic
The _performCheckout method demonstrates how to handle the checkout action. It simulates a confirmation dialog and, upon success, clears the cart and navigates to a `CheckoutScreen`.
CheckoutScreen Placeholder
A simple screen to confirm the checkout was successful.
import 'package:flutter/material.dart';
class CheckoutScreen extends StatelessWidget {
final double orderAmount;
const CheckoutScreen({Key? key, required this.orderAmount}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Checkout Confirmation'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.check_circle_outline,
color: Colors.green,
size: 100,
),
const SizedBox(height: 20),
const Text(
'Your order has been placed successfully!',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Text(
'Total amount: \$${orderAmount.toStringAsFixed(2)}',
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
// Navigate back to the home screen or product listing
Navigator.of(context).popUntil((route) => route.isFirst);
},
child: const Text('Continue Shopping'),
),
],
),
),
);
}
}
4. Using the Widget in Your Application
To make the CartProvider and CartSummaryWidget work, you need to provide the CartProvider at a higher level in your widget tree, typically in main.dart or your main app widget.
main.dart Setup
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app_name/providers/cart_provider.dart'; // Adjust import paths
import 'package:your_app_name/models/product.dart'; // Adjust import paths
import 'package:your_app_name/widgets/cart_summary_widget.dart'; // Adjust import paths
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: 'Shopping Cart Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const HomeScreen(),
),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final cartProvider = Provider.of<CartProvider>(context, listen: false);
// Dummy products for demonstration
final List<Product> products = [
Product(id: 'p1', name: 'Laptop', price: 1200.00),
Product(id: 'p2', name: 'Mouse', price: 25.00),
Product(id: 'p3', name: 'Keyboard', price: 75.00),
Product(id: 'p4', name: 'Monitor', price: 300.00),
];
return Scaffold(
appBar: AppBar(
title: const Text('My Shop'),
actions: [
IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (ctx) => Scaffold(
appBar: AppBar(title: const Text('Your Cart')),
body: const CartSummaryWidget(),
),
),
);
},
),
],
),
body: GridView.builder(
padding: const EdgeInsets.all(10.0),
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 3 / 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemBuilder: (ctx, i) => Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
products[i].name,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text('\$${products[i].price.toStringAsFixed(2)}'),
ElevatedButton(
onPressed: () {
cartProvider.addItem(products[i]);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${products[i].name} added to cart!'),
duration: const Duration(milliseconds: 700),
),
);
},
child: const Text('Add to Cart'),
),
],
),
),
),
);
}
}
Conclusion
You have successfully built a dynamic shopping cart summary widget in Flutter, complete with state management using the provider package and a functional checkout button. This setup provides a solid foundation for any e-commerce application, allowing users to review their selections and proceed to purchase seamlessly.
Further enhancements could include:
- Adding product images and detailed descriptions.
- Implementing a more sophisticated quantity selector with direct input.
- Integrating with a backend API for real order processing and payment gateways.
- Adding animations for cart updates.
- Applying discount codes or shipping options.
By following this guide, you now have a robust and extensible shopping cart solution for your Flutter applications.