Building a Shopping List Widget with Swipe-to-Delete in Flutter
Creating interactive and intuitive user interfaces is crucial for modern mobile applications. A common pattern in list-based apps is the ability to easily remove items. In Flutter, the Dismissible widget provides an elegant solution for implementing swipe-to-delete functionality, offering a satisfying user experience. This article will guide you through building a simple shopping list widget where users can add items and remove them with a swipe gesture.
Prerequisites
Before you begin, ensure you have:
- Flutter SDK installed and configured.
- A basic understanding of Flutter widgets, state management (
StatefulWidget), and lists.
1. The Shopping List Item Data Model
First, let's define a simple data model for our shopping list items. Each item will have a name and a unique ID.
class ShoppingListItem {
final String id;
String name;
bool isCompleted; // Optional: to mark items as bought
ShoppingListItem({required this.id, required this.name, this.isCompleted = false});
}
2. Basic Application Structure
We'll start with a basic Flutter application structure, setting up our main entry point and a MaterialApp.
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart'; // For generating unique IDs
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Shopping List',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const ShoppingListScreen(),
);
}
}
// Data model (add this before ShoppingListScreen or in a separate file)
class ShoppingListItem {
final String id;
String name;
bool isCompleted;
ShoppingListItem({required this.id, required this.name, this.isCompleted = false});
}
3. Creating the ShoppingListScreen
Our shopping list screen will be a StatefulWidget because we need to manage the list of items (add, remove) and update the UI accordingly.
class ShoppingListScreen extends StatefulWidget {
const ShoppingListScreen({super.key});
@override
State createState() => _ShoppingListScreenState();
}
class _ShoppingListScreenState extends State {
final List _shoppingItems = [
ShoppingListItem(id: const Uuid().v4(), name: 'Milk'),
ShoppingListItem(id: const Uuid().v4(), name: 'Bread'),
ShoppingListItem(id: const Uuid().v4(), name: 'Eggs'),
];
final TextEditingController _itemController = TextEditingController();
final Uuid _uuid = const Uuid();
// Method to add a new item
void _addItem(String name) {
if (name.isNotEmpty) {
setState(() {
_shoppingItems.add(ShoppingListItem(id: _uuid.v4(), name: name));
});
_itemController.clear();
}
}
// Method to remove an item (used by Dismissible)
void _removeItem(String id) {
setState(() {
_shoppingItems.removeWhere((item) => item.id == id);
});
}
// Method to toggle completion status (optional)
void _toggleCompletion(String id) {
setState(() {
final index = _shoppingItems.indexWhere((item) => item.id == id);
if (index != -1) {
_shoppingItems[index].isCompleted = !_shoppingItems[index].isCompleted;
}
});
}
@override
void dispose() {
_itemController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Shopping List'),
),
body: ListView.builder(
itemCount: _shoppingItems.length,
itemBuilder: (context, index) {
final item = _shoppingItems[index];
// We'll replace this with Dismissible in the next step
return ListTile(
title: Text(item.name),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddItemDialog(context),
child: const Icon(Icons.add),
),
);
}
// Dialog to add new items
Future _showAddItemDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Add New Item'),
content: TextField(
controller: _itemController,
decoration: const InputDecoration(hintText: 'Enter item name'),
autofocus: true,
onSubmitted: (value) {
_addItem(value);
Navigator.of(context).pop();
},
),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () {
_itemController.clear();
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Add'),
onPressed: () {
_addItem(_itemController.text);
Navigator.of(context).pop();
},
),
],
);
},
);
}
}
Note: You'll need to add the uuid package to your pubspec.yaml for unique IDs:
dependencies:
flutter:
sdk: flutter
uuid: ^4.2.2 # Use the latest version
Then run flutter pub get.
4. Implementing Swipe-to-Delete with Dismissible
Now, let's integrate the Dismissible widget into our ListView.builder. The Dismissible widget requires a unique key, a background widget (what appears behind the item when swiping), and an onDismissed callback.
// ... inside _ShoppingListScreenState's build method ...
body: ListView.builder(
itemCount: _shoppingItems.length,
itemBuilder: (context, index) {
final item = _shoppingItems[index];
return Dismissible(
key: ValueKey(item.id), // A unique key is crucial for Dismissible
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20.0),
child: const Icon(Icons.delete, color: Colors.white),
),
direction: DismissDirection.endToStart, // Only allow swipe from right to left
onDismissed: (direction) {
// Store the item temporarily for undo functionality
final dismissedItem = item;
final dismissedItemIndex = _shoppingItems.indexOf(item);
_removeItem(item.id); // Remove item from our list
// Show a SnackBar with an undo option
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${dismissedItem.name} dismissed'),
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
setState(() {
_shoppingItems.insert(dismissedItemIndex, dismissedItem);
});
},
),
),
);
},
child: Card( // Wrap ListTile in a Card for better visual separation
margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
child: ListTile(
title: Text(
item.name,
style: TextStyle(
decoration: item.isCompleted ? TextDecoration.lineThrough : null,
color: item.isCompleted ? Colors.grey : Colors.black,
),
),
trailing: Checkbox(
value: item.isCompleted,
onChanged: (bool? newValue) {
_toggleCompletion(item.id);
},
),
onTap: () => _toggleCompletion(item.id), // Tap to toggle completion
),
),
);
},
),
// ... rest of the Scaffold ...
Explanation of Dismissible properties:
key: This is mandatory and must be unique for each item.ValueKey(item.id)works perfectly here as ourShoppingListItemhas a uniqueid. Flutter uses this key to correctly identify and animate the widget when it's dismissed or reordered.background: This widget is displayed underneath thechildwhen it is swiped away. We use a redContainerwith a delete icon.direction: Specifies the directions in which the widget can be dismissed.DismissDirection.endToStartallows swiping only from right to left.onDismissed: This callback is invoked after the widget has been dismissed. Inside this callback, we remove the item from our_shoppingItemslist and also display aSnackBarwith an "UNDO" option, allowing the user to reverse the dismissal.child: The actual widget that you want to make dismissible. In our case, it's aCardcontaining aListTilefor each shopping item.
Conclusion
You've now successfully built a functional shopping list widget in Flutter with intuitive swipe-to-delete functionality using the Dismissible widget. This pattern is highly reusable for any list-based application where users need to remove items. You can further enhance this application by adding persistent storage (e.g., using shared_preferences or a database), allowing users to edit items, and implementing more sophisticated state management for larger applications.
The Dismissible widget, combined with ScaffoldMessenger for user feedback, provides a powerful and user-friendly way to manage list items in your Flutter applications.