image

21 Feb 2026

9K

35K

Creating a Task List Widget with Drag & Drop Sorting in Flutter

Building interactive and user-friendly applications is at the heart of modern mobile development. A common requirement for many apps is a task list or any ordered list of items that users can reorder or manage. Flutter provides powerful widgets that make implementing such features, including drag-and-drop sorting, surprisingly straightforward. This article will guide you through creating a dynamic task list widget with drag-and-drop reordering and an optional swipe-to-dismiss functionality using Flutter's built-in capabilities.

Prerequisites

Before diving into the code, ensure you have Flutter installed and set up. You should also be familiar with basic Flutter concepts, including widgets, state management (StatefulWidget), and lists.

1. Project Setup and Data Model

First, create a new Flutter project if you haven't already:


flutter create task_list_app
cd task_list_app

Next, let's define a simple data model for our tasks. We'll include a unique ID, a description, and a boolean for completion status.


class Task {
  final String id;
  String description;
  bool isCompleted;

  Task({
    required this.id,
    required this.description,
    this.isCompleted = false,
  });
}

2. Building the Main Widget Structure

We'll use a StatefulWidget to manage our list of tasks, as the order and properties of tasks will change.


import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart'; // Add uuid package to pubspec.yaml for unique IDs

// Data Model (as defined above)
class Task {
  final String id;
  String description;
  bool isCompleted;

  Task({
    required this.id,
    required this.description,
    this.isCompleted = false,
  });
}

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Task List',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const TaskListScreen(),
    );
  }
}

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

  @override
  State createState() => _TaskListScreenState();
}

class _TaskListScreenState extends State {
  final List _tasks = [
    Task(id: '1', description: 'Buy groceries'),
    Task(id: '2', description: 'Finish Flutter article'),
    Task(id: '3', description: 'Call Mom', isCompleted: true),
    Task(id: '4', description: 'Schedule meeting'),
  ];

  final Uuid _uuid = const Uuid(); // For generating unique IDs

  // ... (methods for reordering, adding, deleting tasks will go here)

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Task List'),
      ),
      body: Container(), // We'll replace this with ReorderableListView
      floatingActionButton: FloatingActionButton(
        onPressed: () {}, // For adding new tasks
        child: const Icon(Icons.add),
      ),
    );
  }
}

Remember to add the uuid package to your pubspec.yaml file:


dependencies:
  flutter:
    sdk: flutter
  uuid: ^4.2.1 # Latest version may vary

Then run flutter pub get.

3. Implementing Drag & Drop with ReorderableListView

Flutter's ReorderableListView is specifically designed for lists whose items can be reordered by the user. It requires a `Key` for each child widget to correctly identify and animate items during reordering.


// Inside _TaskListScreenState class

// Method to handle task reordering
void _onReorder(int oldIndex, int newIndex) {
  setState(() {
    if (newIndex > oldIndex) {
      newIndex -= 1;
    }
    final Task task = _tasks.removeAt(oldIndex);
    _tasks.insert(newIndex, task);
  });
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('My Task List'),
    ),
    body: ReorderableListView.builder(
      itemCount: _tasks.length,
      onReorder: _onReorder,
      itemBuilder: (BuildContext context, int index) {
        final task = _tasks[index];
        return Card(
          key: ValueKey(task.id), // Crucial: Each item needs a unique key
          margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
          child: ListTile(
            title: Text(
              task.description,
              style: TextStyle(
                decoration: task.isCompleted ? TextDecoration.lineThrough : TextDecoration.none,
                color: task.isCompleted ? Colors.grey : Colors.black,
              ),
            ),
            trailing: Checkbox(
              value: task.isCompleted,
              onChanged: (bool? value) {
                setState(() {
                  task.isCompleted = value!;
                });
              },
            ),
            onTap: () {
              // Optionally edit task description
            },
          ),
        );
      },
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: () {
        _addTask();
      },
      child: const Icon(Icons.add),
    ),
  );
}

Key Points for ReorderableListView:

  • itemCount: The total number of items in the list.
  • onReorder: A callback function that is invoked when a drag operation completes. It provides the oldIndex and newIndex of the item. You must update your underlying data model within a setState call to reflect this change. The common pattern is to remove the item from oldIndex and insert it at newIndex.
  • itemBuilder: Similar to ListView.builder, it builds each item. Crucially, each item returned by itemBuilder must have a unique Key. We use ValueKey(task.id) here, assuming task.id is unique.

4. Adding Swipe-to-Dismiss Functionality with Dismissible

To enhance the user experience, we can add a swipe-to-dismiss feature, allowing users to remove tasks by swiping them off the screen. This is achieved by wrapping each list item with a Dismissible widget.


// Inside _TaskListScreenState class

// Method to handle deleting a task
void _deleteTask(String id) {
  setState(() {
    _tasks.removeWhere((task) => task.id == id);
  });
  // Optionally show a SnackBar for undo action
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: const Text('Task dismissed'),
      action: SnackBarAction(
        label: 'UNDO',
        onPressed: () {
          // Implement undo logic if needed
        },
      ),
    ),
  );
}

// ... (inside the build method, replace Card with Dismissible)

body: ReorderableListView.builder(
  itemCount: _tasks.length,
  onReorder: _onReorder,
  itemBuilder: (BuildContext context, int index) {
    final task = _tasks[index];
    return Dismissible(
      key: ValueKey(task.id), // Dismissible also needs a unique key
      direction: DismissDirection.endToStart, // Only swipe from right to left
      background: Container(
        color: Colors.red,
        alignment: Alignment.centerRight,
        padding: const EdgeInsets.symmetric(horizontal: 20.0),
        child: const Icon(Icons.delete, color: Colors.white),
      ),
      onDismissed: (direction) {
        _deleteTask(task.id);
      },
      child: Card( // Wrap your existing Card/ListTile with Dismissible
        margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
        child: ListTile(
          title: Text(
            task.description,
            style: TextStyle(
              decoration: task.isCompleted ? TextDecoration.lineThrough : TextDecoration.none,
              color: task.isCompleted ? Colors.grey : Colors.black,
            ),
          ),
          trailing: Checkbox(
            value: task.isCompleted,
            onChanged: (bool? value) {
              setState(() {
                task.isCompleted = value!;
              });
            },
          ),
          onTap: () {
            // Optionally edit task description
          },
        ),
      ),
    );
  },
),

Key Points for Dismissible:

  • key: Like ReorderableListView items, Dismissible also requires a unique Key. Using the task ID is ideal here too.
  • background: This widget is shown behind the item as it's being swiped. It's common to show a delete icon or a red background.
  • direction: Specifies the allowed swipe directions (e.g., DismissDirection.endToStart for right-to-left swipe).
  • onDismissed: This callback is invoked when the item has been completely swiped off the screen. Here, you should remove the item from your data model.

5. Adding New Tasks

Finally, let's implement the _addTask method to allow users to add new tasks using a simple dialog.


// Inside _TaskListScreenState class

void _addTask() {
  String newTaskDescription = '';
  showDialog(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: const Text('Add New Task'),
        content: TextField(
          autofocus: true,
          onChanged: (value) {
            newTaskDescription = value;
          },
          decoration: const InputDecoration(hintText: 'Enter task description'),
        ),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () {
              if (newTaskDescription.isNotEmpty) {
                setState(() {
                  _tasks.add(Task(id: _uuid.v4(), description: newTaskDescription));
                });
                Navigator.of(context).pop();
              }
            },
            child: const Text('Add'),
          ),
        ],
      );
    },
  );
}

// ... (ensure floatingActionButton calls _addTask)

floatingActionButton: FloatingActionButton(
  onPressed: () {
    _addTask(); // Calls the add task method
  },
  child: const Icon(Icons.add),
),

Complete main.dart File

Here's the full code for your main.dart file, combining all the pieces:


import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';

// Data Model
class Task {
  final String id;
  String description;
  bool isCompleted;

  Task({
    required this.id,
    required this.description,
    this.isCompleted = false,
  });
}

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Task List',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const TaskListScreen(),
    );
  }
}

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

  @override
  State createState() => _TaskListScreenState();
}

class _TaskListScreenState extends State {
  final List _tasks = [
    Task(id: '1', description: 'Buy groceries'),
    Task(id: '2', description: 'Finish Flutter article'),
    Task(id: '3', description: 'Call Mom', isCompleted: true),
    Task(id: '4', description: 'Schedule meeting'),
  ];

  final Uuid _uuid = const Uuid(); // For generating unique IDs

  void _onReorder(int oldIndex, int newIndex) {
    setState(() {
      if (newIndex > oldIndex) {
        newIndex -= 1;
      }
      final Task task = _tasks.removeAt(oldIndex);
      _tasks.insert(newIndex, task);
    });
  }

  void _deleteTask(String id) {
    setState(() {
      _tasks.removeWhere((task) => task.id == id);
    });
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: const Text('Task dismissed'),
        action: SnackBarAction(
          label: 'UNDO',
          onPressed: () {
            // Implement undo logic if needed
          },
        ),
      ),
    );
  }

  void _addTask() {
    String newTaskDescription = '';
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Add New Task'),
          content: TextField(
            autofocus: true,
            onChanged: (value) {
              newTaskDescription = value;
            },
            decoration: const InputDecoration(hintText: 'Enter task description'),
          ),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: const Text('Cancel'),
            ),
            TextButton(
              onPressed: () {
                if (newTaskDescription.isNotEmpty) {
                  setState(() {
                    _tasks.add(Task(id: _uuid.v4(), description: newTaskDescription));
                  });
                  Navigator.of(context).pop();
                }
              },
              child: const Text('Add'),
            ),
          ],
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Task List'),
      ),
      body: ReorderableListView.builder(
        itemCount: _tasks.length,
        onReorder: _onReorder,
        itemBuilder: (BuildContext context, int index) {
          final task = _tasks[index];
          return Dismissible(
            key: ValueKey(task.id),
            direction: DismissDirection.endToStart,
            background: Container(
              color: Colors.red,
              alignment: Alignment.centerRight,
              padding: const EdgeInsets.symmetric(horizontal: 20.0),
              child: const Icon(Icons.delete, color: Colors.white),
            ),
            onDismissed: (direction) {
              _deleteTask(task.id);
            },
            child: Card(
              margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
              child: ListTile(
                title: Text(
                  task.description,
                  style: TextStyle(
                    decoration: task.isCompleted ? TextDecoration.lineThrough : TextDecoration.none,
                    color: task.isCompleted ? Colors.grey : Colors.black,
                  ),
                ),
                trailing: Checkbox(
                  value: task.isCompleted,
                  onChanged: (bool? value) {
                    setState(() {
                      task.isCompleted = value!;
                    });
                  },
                ),
                onTap: () {
                  // Optionally add logic for editing a task when tapped
                },
              ),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _addTask();
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

Conclusion

You have successfully created a dynamic task list widget in Flutter with drag-and-drop reordering and swipe-to-dismiss functionality. This example demonstrates the power and flexibility of Flutter's built-in widgets like ReorderableListView and Dismissible, enabling rich user interactions with minimal code.

From here, you can expand this application by adding features like:

  • Persisting tasks to local storage (e.g., using shared_preferences or sqflite) or a backend.
  • Implementing more sophisticated state management (e.g., Provider, Riverpod, Bloc) for larger applications.
  • Adding more task details (due dates, categories, etc.).
  • Enhancing UI/UX with custom animations and transitions.

This foundation gives you a solid starting point for building highly interactive list-based applications in Flutter.

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