Building a Swipe-to-Remove Shopping Cart Widget in Flutter
Enhancing user experience in e-commerce applications is paramount. One common interaction pattern that significantly improves usability, especially in shopping cart functionalities, is the "swipe-to-remove" gesture. This feature allows users to intuitively remove items from their cart with a simple horizontal swipe, providing immediate feedback and a streamlined workflow. This article will guide you through building a dynamic Flutter shopping cart widget that incorporates this modern interaction using the Dismissible widget.
Core Concepts
Before diving into the implementation, let's briefly touch upon the key Flutter widgets and concepts we'll be utilizing:
StatefulWidget: Essential for managing the mutable state of our shopping cart, such as the list of items.ListView.builder: An efficient way to display a scrollable list of items, especially when the number of items can be large or dynamic.Dismissible: The cornerstone of our swipe-to-remove functionality. It allows a widget to be removed from the tree with a swipe gesture.- State Management (Implicit): For this example, we'll use a local
setStatecall. In larger applications, consider state management solutions like Provider, BLoC, or Riverpod.
Step 1: Define the Cart Item Data Model
First, we need a simple data model for our cart items. This class will hold properties like name, price, and a unique ID.
class CartItem {
final String id;
final String name;
final double price;
int quantity;
CartItem({
required this.id,
required this.name,
required this.price,
this.quantity = 1,
});
// For simplicity, we'll use copyWith for quantity updates
CartItem copyWith({
String? id,
String? name,
double? price,
int? quantity,
}) {
return CartItem(
id: id ?? this.id,
name: name ?? this.name,
price: price ?? this.price,
quantity: quantity ?? this.quantity,
);
}
}
Step 2: Create the Shopping Cart Widget Structure
We'll build a StatefulWidget that maintains a list of CartItem objects. This widget will use ListView.builder to display each item.
import 'package:flutter/material.dart';
// (CartItem class definition from Step 1 goes here)
class ShoppingCartScreen extends StatefulWidget {
@override
_ShoppingCartScreenState createState() => _ShoppingCartScreenState();
}
class _ShoppingCartScreenState extends State {
List<CartItem> _cartItems = [
CartItem(id: 'p1', name: 'Flutter T-Shirt', price: 25.00),
CartItem(id: 'p2', name: 'Dart Mug', price: 12.50, quantity: 2),
CartItem(id: 'p3', name: 'Flutter Sticker Pack', price: 5.00),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My Shopping Cart'),
),
body: _cartItems.isEmpty
? Center(
child: Text(
'Your cart is empty!',
style: TextStyle(fontSize: 18),
),
)
: ListView.builder(
itemCount: _cartItems.length,
itemBuilder: (context, index) {
final item = _cartItems[index];
return Card(
margin: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListTile(
title: Text(item.name),
subtitle: Text(
'Price: \$${item.price.toStringAsFixed(2)} x ${item.quantity}',
),
trailing: Text(
'\$${(item.price * item.quantity).toStringAsFixed(2)}',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 16),
),
),
),
);
},
),
bottomNavigationBar: _buildTotalBar(),
);
}
Widget _buildTotalBar() {
final total = _cartItems.fold(
0.0,
(sum, item) => sum + (item.price * item.quantity),
);
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 5,
offset: Offset(0, -3),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total:',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'\$${total.toStringAsFixed(2)}',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}
Step 3: Implementing Swipe-to-Remove with Dismissible
Now, let's integrate the Dismissible widget. We'll wrap each Card item with Dismissible, providing a unique key, a background visual, and an onDismissed callback.
// (Previous imports and CartItem class)
class ShoppingCartScreen extends StatefulWidget {
@override
_ShoppingCartScreenState createState() => _ShoppingCartScreenState();
}
class _ShoppingCartScreenState extends State<ShoppingCartScreen> {
List<CartItem> _cartItems = [
CartItem(id: 'p1', name: 'Flutter T-Shirt', price: 25.00),
CartItem(id: 'p2', name: 'Dart Mug', price: 12.50, quantity: 2),
CartItem(id: 'p3', name: 'Flutter Sticker Pack', price: 5.00),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My Shopping Cart'),
),
body: _cartItems.isEmpty
? Center(
child: Text(
'Your cart is empty!',
style: TextStyle(fontSize: 18),
),
)
: ListView.builder(
itemCount: _cartItems.length,
itemBuilder: (context, index) {
final item = _cartItems[index];
return Dismissible(
key: ValueKey(item.id), // Unique key is crucial for Dismissible
direction: DismissDirection.endToStart, // Only swipe from right to left
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(
Icons.delete,
color: Colors.white,
size: 30,
),
),
onDismissed: (direction) {
// Store the item temporarily for undo functionality
final removedItem = item;
final removedIndex = index;
setState(() {
_cartItems.removeAt(index);
});
// Show a SnackBar with an undo option
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${removedItem.name} removed from cart.'),
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
setState(() {
_cartItems.insert(removedIndex, removedItem);
});
},
),
),
);
},
child: Card(
margin: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListTile(
title: Text(item.name),
subtitle: Text(
'Price: \$${item.price.toStringAsFixed(2)} x ${item.quantity}',
),
trailing: Text(
'\$${(item.price * item.quantity).toStringAsFixed(2)}',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 16),
),
),
),
),
);
},
),
bottomNavigationBar: _buildTotalBar(),
);
}
// (Remaining _buildTotalBar() method from Step 2)
Widget _buildTotalBar() {
final total = _cartItems.fold(
0.0,
(sum, item) => sum + (item.price * item.quantity),
);
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 5,
offset: Offset(0, -3),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total:',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'\$${total.toStringAsFixed(2)}',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}
Full Code Example
Here is the complete code for a basic Flutter application demonstrating the swipe-to-remove shopping cart widget.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Shopping Cart Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: ShoppingCartScreen(),
);
}
}
class CartItem {
final String id;
final String name;
final double price;
int quantity;
CartItem({
required this.id,
required this.name,
required this.price,
this.quantity = 1,
});
CartItem copyWith({
String? id,
String? name,
double? price,
int? quantity,
}) {
return CartItem(
id: id ?? this.id,
name: name ?? this.name,
price: price ?? this.price,
quantity: quantity ?? this.quantity,
);
}
}
class ShoppingCartScreen extends StatefulWidget {
@override
_ShoppingCartScreenState createState() => _ShoppingCartScreenState();
}
class _ShoppingCartScreenState extends State<ShoppingCartScreen> {
List<CartItem> _cartItems = [
CartItem(id: 'p1', name: 'Flutter T-Shirt', price: 25.00),
CartItem(id: 'p2', name: 'Dart Mug', price: 12.50, quantity: 2),
CartItem(id: 'p3', name: 'Flutter Sticker Pack', price: 5.00),
CartItem(id: 'p4', name: 'Flutter Hoodie', price: 49.99, quantity: 1),
CartItem(id: 'p5', name: 'Dart Keyboard Keycap', price: 8.00, quantity: 3),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My Shopping Cart'),
),
body: _cartItems.isEmpty
? Center(
child: Text(
'Your cart is empty!',
style: TextStyle(fontSize: 18),
),
)
: ListView.builder(
itemCount: _cartItems.length,
itemBuilder: (context, index) {
final item = _cartItems[index];
return Dismissible(
key: ValueKey(item.id), // Unique key for Dismissible
direction: DismissDirection.endToStart, // Swipe from right to left
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(
Icons.delete,
color: Colors.white,
size: 30,
),
),
confirmDismiss: (direction) async {
return await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Confirm Delete"),
content: Text(
"Are you sure you want to remove ${item.name} from your cart?"),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text("CANCEL")),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text("DELETE")),
],
);
},
);
},
onDismissed: (direction) {
final removedItem = item;
final removedIndex = index;
setState(() {
_cartItems.removeAt(index);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${removedItem.name} removed.'),
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
setState(() {
_cartItems.insert(removedIndex, removedItem);
});
},
),
),
);
},
child: Card(
margin: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).primaryColorLight,
child: Text(
item.name[0],
style: TextStyle(color: Theme.of(context).primaryColor),
),
),
title: Text(
item.name,
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
'Price: \$${item.price.toStringAsFixed(2)}',
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.remove_circle_outline),
onPressed: () {
setState(() {
if (item.quantity > 1) {
item.quantity--;
} else {
// Optionally remove item if quantity becomes 0
_cartItems.removeAt(index);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${item.name} removed.'),
),
);
}
});
},
),
Text('${item.quantity}'),
IconButton(
icon: Icon(Icons.add_circle_outline),
onPressed: () {
setState(() {
item.quantity++;
});
},
),
SizedBox(width: 10),
Text(
'\$${(item.price * item.quantity).toStringAsFixed(2)}',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
),
),
),
);
},
),
bottomNavigationBar: _buildTotalBar(),
);
}
Widget _buildTotalBar() {
final total = _cartItems.fold(
0.0,
(sum, item) => sum + (item.price * item.quantity),
);
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 5,
offset: Offset(0, -3),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total:',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'\$${total.toStringAsFixed(2)}',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}
Conclusion
By leveraging Flutter's Dismissible widget, we can easily create intuitive and user-friendly swipe-to-remove functionality for shopping cart items. This approach not only enhances the visual appeal but also streamlines the user's interaction with the application, making the process of managing their cart more efficient and enjoyable. You can further extend this by integrating robust state management solutions, animations, and more complex item manipulation features like changing quantities directly within the list item.