Building a Feature-Rich Task List Widget in Flutter with Tags, Priority, and Swipe Actions
Creating a highly functional and intuitive task list is a common requirement for many mobile applications. Flutter, with its expressive UI toolkit, makes it straightforward to build such components. This article will guide you through the process of developing a sophisticated task list widget that incorporates tags, priority levels, and engaging swipe-to-action functionalities.
1. Defining the Task Data Model
First, we need a robust data model to represent our tasks. Each task will have a unique identifier, a title, an optional description, a list of tags, a priority level, and a completion status.
import 'package:flutter/material.dart'; // Required for @required
enum Priority { low, medium, high }
class Task {
final String id;
final String title;
final String description;
final List<String> tags;
Priority priority;
bool isCompleted;
Task({
@required this.id,
@required this.title,
this.description = '',
this.tags = const [],
this.priority = Priority.medium,
this.isCompleted = false,
});
// A simple copyWith method for immutability (useful for state management)
Task copyWith({
String id,
String title,
String description,
List<String> tags,
Priority priority,
bool isCompleted,
}) {
return Task(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
tags: tags ?? this.tags,
priority: priority ?? this.priority,
isCompleted: isCompleted ?? this.isCompleted,
);
}
}
2. Crafting the Individual Task Item Widget
Next, we'll create a widget that displays a single task, visually representing its tags and priority. We'll use a ListTile as the base and add custom elements.
import 'package:flutter/material.dart';
import 'task_model.dart'; // Assuming task_model.dart contains the Task class
class TaskItem extends StatelessWidget {
final Task task;
final VoidCallback onTap;
const TaskItem({
Key key,
@required this.task,
this.onTap,
}) : super(key: key);
Color _getPriorityColor(Priority priority) {
switch (priority) {
case Priority.low:
return Colors.blue[300];
case Priority.medium:
return Colors.orange[300];
case Priority.high:
return Colors.red[300];
default:
return Colors.grey;
}
}
IconData _getPriorityIcon(Priority priority) {
switch (priority) {
case Priority.low:
return Icons.arrow_downward;
case Priority.medium:
return Icons.sort;
case Priority.high:
return Icons.arrow_upward;
default:
return Icons.priority_high;
}
}
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
elevation: 2,
child: ListTile(
onTap: onTap,
leading: Icon(
task.isCompleted ? Icons.check_circle : Icons.radio_button_unchecked,
color: task.isCompleted ? Colors.green : Colors.grey,
),
title: Text(
task.title,
style: TextStyle(
decoration: task.isCompleted ? TextDecoration.lineThrough : null,
fontStyle: task.isCompleted ? FontStyle.italic : null,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (task.description.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(task.description, style: TextStyle(fontSize: 12)),
),
if (task.tags.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Wrap(
spacing: 6.0,
runSpacing: 4.0,
children: task.tags
.map((tag) => Chip(
label: Text(tag, style: TextStyle(fontSize: 10)),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
))
.toList(),
),
),
],
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_getPriorityIcon(task.priority),
color: _getPriorityColor(task.priority),
size: 18,
),
Text(
task.priority.toString().split('.').last, // e.g., "high"
style: TextStyle(
fontSize: 10,
color: _getPriorityColor(task.priority),
),
),
],
),
),
);
}
}
3. Implementing Swipe Actions with Dismissible
The Dismissible widget is perfect for adding swipe-to-action functionality. We'll wrap our TaskItem with Dismissible to allow users to complete or delete tasks by swiping.
import 'package:flutter/material.dart';
import 'task_model.dart';
import 'task_item.dart'; // Assuming task_item.dart contains the TaskItem widget
class TaskListScreen extends StatefulWidget {
@override
_TaskListScreenState createState() => _TaskListScreenState();
}
class _TaskListScreenState extends State<TaskListScreen> {
List<Task> _tasks = [
Task(
id: '1',
title: 'Buy groceries',
description: 'Milk, bread, eggs, apples',
tags: ['Shopping', 'Home'],
priority: Priority.high,
),
Task(
id: '2',
title: 'Finish Flutter article',
description: 'Write about widgets, tags, and swipe actions',
tags: ['Work', 'Coding'],
priority: Priority.high,
),
Task(
id: '3',
title: 'Call mom',
tags: ['Personal'],
priority: Priority.medium,
),
Task(
id: '4',
title: 'Workout',
description: '30 min cardio',
tags: ['Health'],
priority: Priority.low,
isCompleted: true,
),
];
void _toggleTaskCompletion(String taskId) {
setState(() {
_tasks = _tasks.map((task) {
return task.id == taskId ? task.copyWith(isCompleted: !task.isCompleted) : task;
}).toList();
});
}
void _deleteTask(String taskId) {
setState(() {
_tasks.removeWhere((task) => task.id == taskId);
});
// Optionally show a SnackBar for undo
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Task deleted"),
duration: Duration(seconds: 2),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My Tasks'),
),
body: ListView.builder(
itemCount: _tasks.length,
itemBuilder: (context, index) {
final task = _tasks[index];
return Dismissible(
key: ValueKey(task.id), // Unique key for Dismissible
direction: DismissDirection.horizontal,
background: Container(
color: Colors.green,
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: 20),
child: Icon(Icons.check, color: Colors.white),
),
secondaryBackground: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20),
child: Icon(Icons.delete, color: Colors.white),
),
onDismissed: (direction) {
if (direction == DismissDirection.startToEnd) {
// Swipe from left to right (Complete)
_toggleTaskCompletion(task.id);
} else if (direction == DismissDirection.endToStart) {
// Swipe from right to left (Delete)
_deleteTask(task.id);
}
},
child: TaskItem(
task: task,
onTap: () => _toggleTaskCompletion(task.id), // Tap to toggle completion
),
);
},
),
);
}
}
4. Integrating into a Flutter Application
To see your task list in action, simply set TaskListScreen as the home widget in your main.dart file.
import 'package:flutter/material.dart';
import 'task_list_screen.dart'; // Assuming task_list_screen.dart contains TaskListScreen
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Task List App',
theme: ThemeData(
primarySwatch: Colors.deepPurple,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: TaskListScreen(),
);
}
}
Conclusion
In this article, we've built a powerful and interactive task list widget in Flutter. We started by defining a comprehensive data model for our tasks, then crafted a visually rich TaskItem widget to display priority and tags. Finally, we integrated swipe actions using the Dismissible widget to enable intuitive completion and deletion of tasks.
This foundation can be further extended by adding features such as task editing, filtering, searching, persistent storage (e.g., using shared_preferences or a local database like sqflite), and more sophisticated state management solutions like Provider or Bloc for larger applications.