image

09 Mar 2026

9K

35K

Building a Task List Widget in Flutter with Drag & Drop, Priority, and Due Date

Managing tasks effectively is crucial for productivity, and a well-designed task list application can significantly aid this. In this article, we'll explore how to create a dynamic and interactive task list widget in Flutter, incorporating key features such as drag-and-drop reordering, task priority levels, and due dates. This will leverage Flutter's rich UI capabilities to deliver a seamless user experience.

1. Core Features Overview

Our task list widget will include the following functionalities:

  • Drag & Drop Reordering: Users can intuitively rearrange tasks by dragging them to new positions.
  • Task Priority: Each task can be assigned a priority (e.g., Low, Medium, High), visually indicated by color.
  • Due Date: Tasks can have an optional due date, helping users keep track of deadlines.
  • Adding/Editing Tasks: A mechanism to add new tasks and modify existing ones, including setting their priority and due date.

2. The Task Model

First, let's define a data model for our tasks. We'll use a simple Dart class to encapsulate task properties.


import 'package:flutter/foundation.dart'; // For @required
import 'package:uuid/uuid.dart'; // For unique IDs

enum Priority {
  low,
  medium,
  high,
}

class Task {
  final String id;
  String title;
  Priority priority;
  DateTime? dueDate;
  bool isCompleted;

  Task({
    required this.title,
    this.priority = Priority.medium,
    this.dueDate,
    this.isCompleted = false,
  }) : id = const Uuid().v4(); // Generate a unique ID for each task

  // Helper method for copyWith to update task properties
  Task copyWith({
    String? title,
    Priority? priority,
    DateTime? dueDate,
    bool? isCompleted,
  }) {
    return Task(
      title: title ?? this.title,
      priority: priority ?? this.priority,
      dueDate: dueDate ?? this.dueDate,
      isCompleted: isCompleted ?? this.isCompleted,
    );
  }
}

3. Main Task List Widget Structure

Our main widget will be a StatefulWidget to manage the list of tasks and their state. We'll use ReorderableListView for the drag-and-drop functionality.


import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; // For date formatting

// Assuming Task and Priority enum are defined in 'task.dart'
// import 'task.dart'; 

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

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

class _TaskListScreenState extends State {
  final List<Task> _tasks = [
    Task(title: 'Buy groceries', priority: Priority.high, dueDate: DateTime.now().add(const Duration(days: 1))),
    Task(title: 'Finish Flutter article', priority: Priority.medium, dueDate: DateTime.now()),
    Task(title: 'Call mom', priority: Priority.low),
    Task(title: 'Exercise', priority: Priority.medium, isCompleted: true),
  ];

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

  void _addTask(Task newTask) {
    setState(() {
      _tasks.add(newTask);
    });
  }

  void _updateTask(Task updatedTask) {
    setState(() {
      final index = _tasks.indexWhere((task) => task.id == updatedTask.id);
      if (index != -1) {
        _tasks[index] = updatedTask;
      }
    });
  }

  void _deleteTask(String id) {
    setState(() {
      _tasks.removeWhere((task) => task.id == id);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Task List'),
      ),
      body: ReorderableListView.builder(
        itemCount: _tasks.length,
        itemBuilder: (context, index) {
          final task = _tasks[index];
          return TaskListItem(
            key: ValueKey(task.id), // Important for ReorderableListView
            task: task,
            onToggleComplete: (value) {
              _updateTask(task.copyWith(isCompleted: value));
            },
            onEdit: () => _showTaskForm(context, task: task),
            onDelete: () => _deleteTask(task.id),
          );
        },
        onReorder: _onReorder,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showTaskForm(context),
        child: const Icon(Icons.add),
      ),
    );
  }

  // ... (TaskFormDialog implementation will go here)
  void _showTaskForm(BuildContext context, {Task? task}) async {
    final result = await showModalBottomSheet<Task>(
      context: context,
      isScrollControlled: true,
      builder: (_) => TaskFormDialog(task: task),
    );

    if (result != null) {
      if (task == null) {
        _addTask(result);
      } else {
        _updateTask(result);
      }
    }
  }
}

4. Task List Item Widget

Each task in the list will be represented by a TaskListItem widget. This widget will display the task's title, priority, due date, and a checkbox for completion. It will also have actions for editing and deleting.


// Inside _TaskListScreenState or as a separate widget file
class TaskListItem extends StatelessWidget {
  final Task task;
  final ValueChanged<bool> onToggleComplete;
  final VoidCallback onEdit;
  final VoidCallback onDelete;

  const TaskListItem({
    super.key,
    required this.task,
    required this.onToggleComplete,
    required this.onEdit,
    required this.onDelete,
  });

  Color _getPriorityColor(Priority priority) {
    switch (priority) {
      case Priority.high:
        return Colors.red[700]!;
      case Priority.medium:
        return Colors.orange[700]!;
      case Priority.low:
        return Colors.green[700]!;
      default:
        return Colors.grey;
    }
  }

  @override
  Widget build(BuildContext context) {
    final priorityColor = _getPriorityColor(task.priority);
    final textDecoration = task.isCompleted ? TextDecoration.lineThrough : TextDecoration.none;

    return Card(
      margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
      child: ListTile(
        leading: Checkbox(
          value: task.isCompleted,
          onChanged: (value) => onToggleComplete(value ?? false),
        ),
        title: Text(
          task.title,
          style: TextStyle(
            decoration: textDecoration,
            fontWeight: FontWeight.bold,
          ),
        ),
        subtitle: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(Icons.flag, size: 16, color: priorityColor),
                const SizedBox(width: 4),
                Text(
                  'Priority: ${task.priority.name.toUpperCase()}',
                  style: TextStyle(color: priorityColor),
                ),
              ],
            ),
            if (task.dueDate != null)
              Padding(
                padding: const EdgeInsets.only(top: 4.0),
                child: Row(
                  children: [
                    const Icon(Icons.calendar_today, size: 16),
                    const SizedBox(width: 4),
                    Text('Due: ${DateFormat.yMMMd().format(task.dueDate!)}'),
                  ],
                ),
              ),
          ],
        ),
        trailing: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            IconButton(
              icon: const Icon(Icons.edit, color: Colors.blue),
              onPressed: onEdit,
            ),
            IconButton(
              icon: const Icon(Icons.delete, color: Colors.red),
              onPressed: onDelete,
            ),
            // ReorderableListView needs a draggable handle, often part of the item.
            // A simple way is to wrap the entire ListTile in a ReorderableDragStartListener,
            // or provide an explicit drag handle. For simplicity, ReorderableListView
            // can infer a drag handle if the item is a ListTile, or a custom widget.
            // For custom items, wrapping with ReorderableDragStartListener is a good approach.
            // Or just make the ListTile itself draggable.
          ],
        ),
      ),
    );
  }
}

5. Task Form Dialog for Adding/Editing

To add new tasks or edit existing ones, we'll implement a modal bottom sheet containing a form. This form will allow users to input the task title, select a priority, and choose a due date using a date picker.


// TaskFormDialog widget
class TaskFormDialog extends StatefulWidget {
  final Task? task; // Optional: if provided, it's for editing an existing task

  const TaskFormDialog({super.key, this.task});

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

class _TaskFormDialogState extends State {
  final _formKey = GlobalKey();
  late TextEditingController _titleController;
  late Priority _selectedPriority;
  DateTime? _selectedDueDate;

  @override
  void initState() {
    super.initState();
    _titleController = TextEditingController(text: widget.task?.title ?? '');
    _selectedPriority = widget.task?.priority ?? Priority.medium;
    _selectedDueDate = widget.task?.dueDate;
  }

  @override
  void dispose() {
    _titleController.dispose();
    super.dispose();
  }

  Future<void> _pickDueDate() async {
    final DateTime? picked = await showDatePicker(
      context: context,
      initialDate: _selectedDueDate ?? DateTime.now(),
      firstDate: DateTime(2000),
      lastDate: DateTime(2101),
    );
    if (picked != null && picked != _selectedDueDate) {
      setState(() {
        _selectedDueDate = picked;
      });
    }
  }

  void _submitForm() {
    if (_formKey.currentState!.validate()) {
      final String title = _titleController.text;
      final Priority priority = _selectedPriority;
      final DateTime? dueDate = _selectedDueDate;

      Task resultTask;
      if (widget.task == null) {
        // New task
        resultTask = Task(title: title, priority: priority, dueDate: dueDate);
      } else {
        // Edit existing task
        resultTask = widget.task!.copyWith(
          title: title,
          priority: priority,
          dueDate: dueDate,
        );
      }
      Navigator.of(context).pop(resultTask);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.only(
        top: 20,
        left: 20,
        right: 20,
        bottom: MediaQuery.of(context).viewInsets.bottom + 20,
      ),
      child: Form(
        key: _formKey,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              widget.task == null ? 'Add New Task' : 'Edit Task',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 20),
            TextFormField(
              controller: _titleController,
              decoration: const InputDecoration(
                labelText: 'Task Title',
                border: OutlineInputBorder(),
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter a task title.';
                }
                return null;
              },
            ),
            const SizedBox(height: 20),
            DropdownButtonFormField<Priority>(
              value: _selectedPriority,
              decoration: const InputDecoration(
                labelText: 'Priority',
                border: OutlineInputBorder(),
              ),
              items: Priority.values.map((priority) {
                return DropdownMenuItem(
                  value: priority,
                  child: Text(priority.name.toUpperCase()),
                );
              }).toList(),
              onChanged: (value) {
                setState(() {
                  _selectedPriority = value!;
                });
              },
            ),
            const SizedBox(height: 20),
            ListTile(
              title: Text(_selectedDueDate == null
                  ? 'Select Due Date'
                  : 'Due Date: ${DateFormat.yMMMd().format(_selectedDueDate!)}'),
              trailing: const Icon(Icons.calendar_today),
              onTap: _pickDueDate,
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _submitForm,
              child: Text(widget.task == null ? 'Add Task' : 'Save Changes'),
            ),
          ],
        ),
      ),
    );
  }
}

6. Putting It All Together

To run this code, ensure you have the following dependencies in your pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  intl: ^0.18.0 # For date formatting
  uuid: ^4.0.0 # For generating unique IDs

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

Then, run flutter pub get. You can then use TaskListScreen as the home widget in your MaterialApp:


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

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

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

Conclusion

By combining Flutter's ReorderableListView with custom state management and form handling, we've created a functional and interactive task list widget. Users can efficiently manage their tasks by reordering them via drag & drop, prioritizing important items, and setting due dates to meet deadlines. This example demonstrates Flutter's power in building rich, responsive, and user-friendly applications with relatively straightforward code.

Further enhancements could include persistent storage (e.g., using shared_preferences, SQLite with sqflite, or a cloud database), task filtering, and more sophisticated notification systems for due dates.

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