image

15 Jan 2026

9K

35K

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 our ShoppingListItem has a unique id. Flutter uses this key to correctly identify and animate the widget when it's dismissed or reordered.
  • background: This widget is displayed underneath the child when it is swiped away. We use a red Container with a delete icon.
  • direction: Specifies the directions in which the widget can be dismissed. DismissDirection.endToStart allows 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 _shoppingItems list and also display a SnackBar with 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 a Card containing a ListTile for 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.

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