GDG Moi University

Flutter with AI

Google Developer Group
Moi University

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
To-Do List App Preview

Step 1: Project Setup

Let's start by creating a new Flutter project.

flutter create todo_app cd todo_app

This creates a new Flutter project with the default counter app template. Let's clean up the main.dart file and start fresh.

lib/main.dart
dart
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.

lib/models/task.dart
dart
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.

lib/models/task_provider.dart
dart
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:

pubspec.yaml (partial)
yaml
dependencies: flutter: sdk: flutter uuid: ^3.0.7

Then 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.

lib/todo_list_screen.dart
dart
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.

lib/task_detail_screen.dart
dart
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.

lib/main.dart
dart
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 run

You 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:

pubspec.yaml (partial)
yaml
dependencies: flutter: sdk: flutter uuid: ^3.0.7 shared_preferences: ^2.2.0

Then, update your task_provider.dart to save and load tasks:

lib/models/task_provider.dart
dart
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:

lib/todo_list_screen.dart (partial)
dart
@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.