Building a To-Do List App
A hands-on project to apply your Flutter knowledge
Project Overview
In this project, we'll build a complete to-do list application for Android using Flutter. This app will allow users to:
- View a list of tasks
- Add new tasks
- Mark tasks as completed
- Delete tasks
- View task details on a separate screen
Step 1: Project Setup
Let's start by creating a new Flutter project.
flutter create todo_app cd todo_appThis creates a new Flutter project with the default counter app template. Let's clean up the main.dart file and start fresh.
import 'package:flutter/material.dart'; void main() { runApp(const TodoApp()); } class TodoApp extends StatelessWidget { const TodoApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Todo App', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const TodoListScreen(), ); } } class TodoListScreen extends StatelessWidget { const TodoListScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Todo List'), ), body: const Center( child: Text('Todo List App'), ), ); } }Step 2: Creating the Task Model
Let's create a model class to represent a task in our to-do list.
class Task { final String id; final String title; final String description; bool isCompleted; Task({ required this.id, required this.title, required this.description, this.isCompleted = false, }); }Now, let's create a simple data provider to manage our tasks. In a real app, you might use a database or API, but for simplicity, we'll use an in-memory list.
import 'package:uuid/uuid.dart'; import 'task.dart'; class TaskProvider { // Singleton pattern static final TaskProvider _instance = TaskProvider._internal(); factory TaskProvider() { return _instance; } TaskProvider._internal(); final List<Task> _tasks = []; final _uuid = Uuid(); List<Task> get tasks => List.unmodifiable(_tasks); void addTask(String title, String description) { final task = Task( id: _uuid.v4(), title: title, description: description, ); _tasks.add(task); } void toggleTaskCompletion(String id) { final taskIndex = _tasks.indexWhere((task) => task.id == id); if (taskIndex != -1) { _tasks[taskIndex].isCompleted = !_tasks[taskIndex].isCompleted; } } void deleteTask(String id) { _tasks.removeWhere((task) => task.id == id); } Task? getTask(String id) { try { return _tasks.firstWhere((task) => task.id == id); } catch (e) { return null; } } }Note: You'll need to add the uuid package to your pubspec.yaml file:
dependencies: flutter: sdk: flutter uuid: ^3.0.7Then run flutter pub get to install the dependency.
Step 3: Building the Task List Screen
Now, let's update our TodoListScreen to display a list of tasks.
import 'package:flutter/material.dart'; import 'models/task.dart'; import 'models/task_provider.dart'; import 'task_detail_screen.dart'; class TodoListScreen extends StatefulWidget { const TodoListScreen({Key? key}) : super(key: key); @override _TodoListScreenState createState() => _TodoListScreenState(); } class _TodoListScreenState extends State<TodoListScreen> { final TaskProvider _taskProvider = TaskProvider(); final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); @override void initState() { super.initState(); // Add some sample tasks if (_taskProvider.tasks.isEmpty) { _taskProvider.addTask( 'Buy groceries', 'Milk, eggs, bread, and fruits', ); _taskProvider.addTask( 'Finish Flutter course', 'Complete the to-do list project', ); _taskProvider.addTask( 'Go for a run', '30 minutes of jogging in the park', ); } } @override void dispose() { _titleController.dispose(); _descriptionController.dispose(); super.dispose(); } void _addTask() { showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Add Task'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _titleController, decoration: const InputDecoration( labelText: 'Title', ), ), TextField( controller: _descriptionController, decoration: const InputDecoration( labelText: 'Description', ), ), ], ), actions: [ TextButton( onPressed: () { Navigator.pop(context); }, child: const Text('Cancel'), ), TextButton( onPressed: () { if (_titleController.text.isNotEmpty) { setState(() { _taskProvider.addTask( _titleController.text, _descriptionController.text, ); _titleController.clear(); _descriptionController.clear(); }); Navigator.pop(context); } }, child: const Text('Add'), ), ], ); }, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Todo List'), ), body: _taskProvider.tasks.isEmpty ? const Center( child: Text('No tasks yet. Add some!'), ) : ListView.builder( itemCount: _taskProvider.tasks.length, itemBuilder: (context, index) { final task = _taskProvider.tasks[index]; return ListTile( leading: Checkbox( value: task.isCompleted, onChanged: (value) { setState(() { _taskProvider.toggleTaskCompletion(task.id); }); }, ), title: Text( task.title, style: TextStyle( decoration: task.isCompleted ? TextDecoration.lineThrough : null, ), ), subtitle: Text(task.description), trailing: IconButton( icon: const Icon(Icons.delete), onPressed: () { setState(() { _taskProvider.deleteTask(task.id); }); }, ), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => TaskDetailScreen(taskId: task.id), ), ); }, ); }, ), floatingActionButton: FloatingActionButton( onPressed: _addTask, child: const Icon(Icons.add), ), ); } }Step 4: Creating the Task Detail Screen
Let's create a screen to display the details of a task when the user taps on it.
import 'package:flutter/material.dart'; import 'models/task.dart'; import 'models/task_provider.dart'; class TaskDetailScreen extends StatelessWidget { final String taskId; final TaskProvider _taskProvider = TaskProvider(); TaskDetailScreen({Key? key, required this.taskId}) : super(key: key); @override Widget build(BuildContext context) { final task = _taskProvider.getTask(taskId); if (task == null) { return Scaffold( appBar: AppBar( title: const Text('Task Not Found'), ), body: const Center( child: Text('The task you are looking for does not exist.'), ), ); } return Scaffold( appBar: AppBar( title: const Text('Task Details'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( task.title, style: Theme.of(context).textTheme.headline5, ), const SizedBox(height: 8), Text( task.description, style: Theme.of(context).textTheme.bodyText1, ), const SizedBox(height: 16), Row( children: [ const Text('Status: '), Chip( label: Text( task.isCompleted ? 'Completed' : 'Pending', ), backgroundColor: task.isCompleted ? Colors.green.shade100 : Colors.orange.shade100, ), ], ), ], ), ), ); } }Step 5: Updating the Main App
Now, let's update our main.dart file to use our new screens.
import 'package:flutter/material.dart'; import 'todo_list_screen.dart'; void main() { runApp(const TodoApp()); } class TodoApp extends StatelessWidget { const TodoApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Todo App', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const TodoListScreen(), ); } }Step 6: Running the App
Now, let's run our app to see it in action.
flutter runYou should see a to-do list app with the ability to:
- View a list of tasks
- Add new tasks by tapping the floating action button
- Mark tasks as completed by tapping the checkbox
- Delete tasks by tapping the delete icon
- View task details by tapping on a task
Step 7: Enhancements (Optional)
Here are some enhancements you can make to the app:
1. Persistent Storage
Currently, our app loses all tasks when it's closed. Let's add persistent storage using the shared_preferences package.
First, add the package to your pubspec.yaml:
dependencies: flutter: sdk: flutter uuid: ^3.0.7 shared_preferences: ^2.2.0Then, update your task_provider.dart to save and load tasks:
import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; import 'task.dart'; class TaskProvider { static final TaskProvider _instance = TaskProvider._internal(); factory TaskProvider() { return _instance; } TaskProvider._internal(); List<Task> _tasks = []; final _uuid = Uuid(); static const String _prefsKey = 'tasks'; List<Task> get tasks => List.unmodifiable(_tasks); // Load tasks from SharedPreferences Future<void> loadTasks() async { final prefs = await SharedPreferences.getInstance(); final tasksJson = prefs.getStringList(_prefsKey) ?? []; _tasks = tasksJson.map((taskJson) { final taskMap = jsonDecode(taskJson) as Map<String, dynamic>; return Task( id: taskMap['id'] as String, title: taskMap['title'] as String, description: taskMap['description'] as String, isCompleted: taskMap['isCompleted'] as bool, ); }).toList(); } // Save tasks to SharedPreferences Future<void> _saveTasks() async { final prefs = await SharedPreferences.getInstance(); final tasksJson = _tasks.map((task) { return jsonEncode({ 'id': task.id, 'title': task.title, 'description': task.description, 'isCompleted': task.isCompleted, }); }).toList(); await prefs.setStringList(_prefsKey, tasksJson); } Future<void> addTask(String title, String description) async { final task = Task( id: _uuid.v4(), title: title, description: description, ); _tasks.add(task); await _saveTasks(); } Future<void> toggleTaskCompletion(String id) async { final taskIndex = _tasks.indexWhere((task) => task.id == id); if (taskIndex != -1) { _tasks[taskIndex].isCompleted = !_tasks[taskIndex].isCompleted; await _saveTasks(); } } Future<void> deleteTask(String id) async { _tasks.removeWhere((task) => task.id == id); await _saveTasks(); } Task? getTask(String id) { try { return _tasks.firstWhere((task) => task.id == id); } catch (e) { return null; } } }You'll also need to update your TodoListScreen to load tasks when the app starts:
@override void initState() { super.initState(); _loadTasks(); } Future<void> _loadTasks() async { await _taskProvider.loadTasks(); setState(() {}); // Add sample tasks only if there are no tasks after loading if (_taskProvider.tasks.isEmpty) { await _taskProvider.addTask( 'Buy groceries', 'Milk, eggs, bread, and fruits', ); await _taskProvider.addTask( 'Finish Flutter course', 'Complete the to-do list project', ); await _taskProvider.addTask( 'Go for a run', '30 minutes of jogging in the park', ); setState(() {}); } }2. Task Categories
Add the ability to categorize tasks (e.g., work, personal, shopping) and filter the task list by category.
3. Due Dates
Add due dates to tasks and sort the task list by due date.
4. Task Search
Add a search bar to filter tasks by title or description.
5. UI Improvements
Enhance the UI with custom themes, animations, and more polished design.
Pro Tip
For a more robust state management solution, consider refactoring the app to use Provider, Riverpod, or Bloc. These libraries make it easier to manage state as your app grows in complexity.