Building a Task List Widget with Swipe Action and Priority Tag in Flutter
Flutter, Google's UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, offers powerful widgets and a declarative approach to UI development. In this article, we'll walk through creating an interactive task list widget. This widget will feature swipe-to-dismiss actions for marking tasks as complete or deleting them, and visually distinct priority tags to highlight important tasks.
1. Defining the Task Model
First, let's define a simple data model for our tasks. This model will hold properties like a unique ID, title, description, a priority level, and a completion status.
Priority Enum
We'll start with an enumeration for task priorities.
enum Priority {
low,
medium,
high,
}
Task Class
Next, the Task class:
class Task {
final String id;
String title;
String description;
Priority priority;
bool isCompleted;
Task({
required this.id,
required this.title,
this.description = '',
this.priority = Priority.low,
this.isCompleted = false,
});
// For immutability and easy updates
Task copyWith({
String? id,
String? title,
String? description,
Priority? priority,
bool? isCompleted,
}) {
return Task(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
priority: priority ?? this.priority,
isCompleted: isCompleted ?? this.isCompleted,
);
}
}
2. Designing the Task Item Widget
The task item widget will be responsible for displaying a single task's details, including its title, description, and a visual indicator for its priority. We'll use ListTile for the main layout and a custom Container for the priority tag.
import 'package:flutter/material.dart';
// Assuming task_model.dart contains the Task and Priority definitions
import 'package:your_project_name/task_model.dart';
class TaskItem extends StatelessWidget {
final Task task;
final VoidCallback? onToggleComplete;
const TaskItem({
Key? key,
required this.task,
this.onToggleComplete,
}) : super(key: key);
Color _getPriorityColor(Priority priority) {
switch (priority) {
case Priority.low:
return Colors.blue.shade200;
case Priority.medium:
return Colors.orange.shade300;
case Priority.high:
return Colors.red.shade400;
default:
return Colors.grey;
}
}
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
color: task.isCompleted ? Colors.grey.shade200 : Colors.white,
elevation: 2,
child: ListTile(
leading: Checkbox(
value: task.isCompleted,
onChanged: (_) => onToggleComplete?.call(),
activeColor: Colors.green,
),
title: Text(
task.title,
style: TextStyle(
decoration: task.isCompleted ? TextDecoration.lineThrough : TextDecoration.none,
color: task.isCompleted ? Colors.grey.shade700 : Colors.black,
fontWeight: FontWeight.w500,
),
),
subtitle: task.description.isNotEmpty
? Text(
task.description,
style: TextStyle(
decoration: task.isCompleted ? TextDecoration.lineThrough : TextDecoration.none,
color: task.isCompleted ? Colors.grey.shade600 : Colors.black54,
),
)
: null,
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
decoration: BoxDecoration(
color: _getPriorityColor(task.priority),
borderRadius: BorderRadius.circular(5.0),
),
child: Text(
task.priority.name.toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
onTap: () {
// Future expansion: Could open a detail screen or edit dialog
},
),
);
}
}
3. Implementing Swipe Actions with Dismissible
Flutter's Dismissible widget makes it incredibly easy to add swipe-to-dismiss functionality to list items. We'll wrap our TaskItem with Dismissible to allow users to swipe left to delete or swipe right to mark as complete.
import 'package:flutter/material.dart';
import 'package:your_project_name/task_model.dart';
import 'package:your_project_name/task_item.dart'; // Assuming TaskItem is in task_item.dart
class TaskListScreen extends StatefulWidget {
const TaskListScreen({Key? key}) : super(key: key);
@override
State createState() => _TaskListScreenState();
}
class _TaskListScreenState extends State {
// Sample data. In a real application, this would come from a database or API.
final List _tasks = [
Task(id: '1', title: 'Buy groceries', description: 'Milk, Eggs, Bread', priority: Priority.high),
Task(id: '2', title: 'Finish Flutter article', priority: Priority.high, isCompleted: false),
Task(id: '3', title: 'Call mom', priority: Priority.medium),
Task(id: '4', title: 'Go for a run', priority: Priority.low),
];
void _deleteTask(String id) {
setState(() {
_tasks.removeWhere((task) => task.id == id);
});
}
void _toggleTaskComplete(String id) {
setState(() {
final index = _tasks.indexWhere((task) => task.id == id);
if (index != -1) {
_tasks[index] = _tasks[index].copyWith(isCompleted: !_tasks[index].isCompleted);
}
});
}
void _addNewTask() {
setState(() {
_tasks.add(Task(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: 'New Task ${_tasks.length + 1}',
description: 'A newly added task',
priority: Priority.low,
isCompleted: false,
));
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Task List'),
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white, // For better contrast with blueAccent
),
body: _tasks.isEmpty
? const Center(child: Text('No tasks yet! Add some.'))
: ListView.builder(
itemCount: _tasks.length,
itemBuilder: (context, index) {
final task = _tasks[index];
return Dismissible(
key: Key(task.id), // Unique key is crucial for Dismissible
direction: DismissDirection.horizontal,
background: Container(
color: Colors.green,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: const Icon(Icons.check, color: Colors.white, size: 30),
),
secondaryBackground: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: const Icon(Icons.delete, color: Colors.white, size: 30),
),
confirmDismiss: (direction) async {
if (direction == DismissDirection.endToStart) {
// Swiping left (end to start) to delete: show confirmation dialog
return await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Confirm Deletion"),
content: const Text("Are you sure you want to delete this task?"),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text("Cancel"),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text("Delete"),
),
],
);
},
);
}
// Swiping right (start to end) to mark as complete: no confirmation needed
return Future.value(true);
},
onDismissed: (direction) {
if (direction == DismissDirection.endToStart) {
_deleteTask(task.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('"${task.title}" deleted')),
);
} else {
_toggleTaskComplete(task.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'"${task.title}" marked as ${task.isCompleted ? 'incomplete' : 'complete'}')),
);
}
},
child: TaskItem(
task: task,
onToggleComplete: () => _toggleTaskComplete(task.id),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addNewTask,
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
child: const Icon(Icons.add),
),
);
}
}
4. Integrating into the Main Application
Finally, let's wire everything up in our main.dart file to run our new task list application.
import 'package:flutter/material.dart';
import 'package:your_project_name/task_list_screen.dart'; // Assuming TaskListScreen is in task_list_screen.dart
// import 'package:your_project_name/task_model.dart'; // Not strictly needed here if TaskListScreen encapsulates it
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Task List',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
),
),
home: const TaskListScreen(),
);
}
}
Conclusion
In this article, we've successfully built a dynamic task list widget in Flutter featuring key functionalities like priority tags and interactive swipe actions. We covered defining a data model, designing individual task items, and integrating the powerful Dismissible widget for an intuitive user experience.
This foundation can be expanded further by adding features such as task editing, persistent storage (using packages like shared_preferences or a database like SQLite/Hive), filtering/sorting tasks, and more advanced state management solutions (e.g., Provider, BLoC, Riverpod) for larger applications.