Build a Flutter To-Do List App
A To-Do List App is a practical Flutter project that helps users create, view, complete, and delete tasks. This project is useful for learning Flutter widgets, user input, lists, state management, event handling, and basic application structure.
In this project, we will build a simple To-Do List application where users can enter a task, add it to a list, mark tasks as completed, and delete tasks.
1. What is a To-Do List App?
A To-Do List App is an application used to organize tasks and activities. Each task can contain information such as a title, completion status, and optionally a description or due date.
For example:
- Learn Flutter
- Complete Dart practice
- Build a Flutter project
- Read documentation
- Submit assignment
2. Objectives of This Project
After completing this project, you will understand how to:
- Create a Flutter application.
- Use StatefulWidget for dynamic UI.
- Manage application data using a list.
- Accept user input using TextField.
- Use TextEditingController.
- Add items to a list.
- Display data using ListView.builder.
- Use Checkbox to mark tasks as completed.
- Delete tasks from a list.
- Update the UI using setState().
- Create reusable widgets and methods.
- Validate empty user input.
- Build a complete mini Flutter project.
3. Technologies Used
| Technology | Purpose |
|---|
| Flutter | UI framework for building the application |
| Dart | Programming language used by Flutter |
| Material Design | Provides ready-made UI components |
| StatefulWidget | Manages changing application state |
| TextField | Accepts task input from the user |
| ListView.builder | Displays tasks efficiently in a list |
| Checkbox | Marks a task as completed |
4. Basic App Flow
User opens application
↓
To-Do List screen appears
↓
User enters a task
↓
User presses Add button
↓
Task is added to the list
↓
ListView displays the task
↓
User can mark task as completed
↓
User can delete task
↓
UI updates automatically
5. Why StatefulWidget is Required
A To-Do List is dynamic because the list changes when the user adds, completes, or deletes a task. Flutter uses StatefulWidget when widget data or appearance needs to change during its lifetime.
The mutable data is stored in the State object, and setState() tells Flutter that the state has changed and the UI needs to be rebuilt.
class TodoPage extends StatefulWidget {
const TodoPage({super.key});
@override
State createState() => _TodoPageState();
}
class _TodoPageState extends State {
List tasks = [];
@override
Widget build(BuildContext context) {
return Container();
}
}
6. Create a Flutter Project
Open the terminal and create a new Flutter project.
flutter create todo_app
Move into the project directory:
cd todo_app
Run the application:
flutter run
7. Project Structure
todo_app/
├── android/
├── ios/
├── lib/
│ └── main.dart
├── test/
├── web/
├── pubspec.yaml
└── README.md
For this beginner project, most of the application logic can be implemented inside lib/main.dart.
8. Import Flutter Material Package
import 'package:flutter/material.dart';
This provides commonly used Flutter Material widgets such as Scaffold, AppBar, TextField, ListView, Checkbox, IconButton, FloatingActionButton, and more.
9. Create the Main Function
void main() {
runApp(const TodoApp());
}
The main() function is the starting point of the Dart application. runApp() places the root widget into the Flutter widget tree.
10. Create the Root Application
class TodoApp extends StatelessWidget {
const TodoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'To-Do List',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
),
useMaterial3: true,
),
home: const TodoPage(),
);
}
}
11. Create the To-Do Page
class TodoPage extends StatefulWidget {
const TodoPage({super.key});
@override
State createState() => _TodoPageState();
}
The TodoPage uses StatefulWidget because the task list and completion status change during runtime.
12. Create the Task Model
A model class makes it easier to store task information. Instead of storing only strings, we can store the task title and whether it has been completed.
class Todo {
String title;
bool isCompleted;
Todo({
required this.title,
this.isCompleted = false,
});
}
13. Store Tasks in a List
class _TodoPageState extends State {
final List _todos = [];
final TextEditingController _controller =
TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(),
);
}
}
The _todos list stores all tasks. Each Todo object contains the task title and completion status.
14. TextEditingController
TextEditingController is used to read and control the text entered into a TextField.
final TextEditingController _controller =
TextEditingController();
For example, if the user enters Learn Flutter, the value can be accessed using:
_controller.text
The controller should be disposed when the State object is removed from the widget tree.
@override
void dispose() {
_controller.dispose();
super.dispose();
}
15. Create the TextField
TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Enter a task',
border: OutlineInputBorder(),
),
)
The TextField allows the user to type a new task.
16. Add Task Button
We can use an ElevatedButton or IconButton to add the entered task.
ElevatedButton(
onPressed: _addTodo,
child: const Text('Add Task'),
)
17. Create the Add Task Method
void _addTodo() {
final text = _controller.text.trim();
if (text.isEmpty) {
return;
}
setState(() {
_todos.add(
Todo(title: text),
);
});
_controller.clear();
}
The method performs the following operations:
- Reads the text from the TextField.
- Removes unnecessary spaces using trim().
- Checks whether the input is empty.
- Creates a Todo object.
- Adds the Todo object to the list.
- Calls setState() to update the UI.
- Clears the input field.
18. Why setState() is Important
When the _todos list changes, Flutter needs to know that the UI should be rebuilt. The modification should therefore be performed inside setState().
setState(() {
_todos.add(
Todo(title: text),
);
});
Without setState(), the list data may change internally but the visible UI will not automatically reflect that change.
19. Display Tasks Using ListView.builder
Expanded(
child: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
final todo = _todos[index];
return ListTile(
title: Text(todo.title),
);
},
),
)
ListView.builder creates list items based on the number of tasks available in the _todos list.
20. Create a Checkbox for Completion
Checkbox(
value: todo.isCompleted,
onChanged: (value) {
setState(() {
todo.isCompleted = value ?? false;
});
},
)
When the checkbox is selected, the task's isCompleted value becomes true. When it is unselected, the value becomes false.
21. Show Completed Tasks with Different Styling
Completed tasks can be displayed using a line-through decoration.
Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
),
)
22. Delete a Task
We can use an IconButton to delete a task.
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
_todos.removeAt(index);
});
},
)
The removeAt() method removes the item at the specified index.
23. Complete Task List Item
ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (value) {
setState(() {
todo.isCompleted = value ?? false;
});
},
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
_todos.removeAt(index);
});
},
),
)
24. Complete To-Do List UI
Scaffold(
appBar: AppBar(
title: const Text('My To-Do List'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Enter a task',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _addTodo(),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _addTodo,
child: const Text('Add'),
),
],
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
final todo = _todos[index];
return Card(
child: ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (value) {
setState(() {
todo.isCompleted =
value ?? false;
});
},
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
_todos.removeAt(index);
});
},
),
),
);
},
),
),
],
),
),
)
25. Complete Flutter To-Do List Application
import 'package:flutter/material.dart';
void main() {
runApp(const TodoApp());
}
class Todo {
String title;
bool isCompleted;
Todo({
required this.title,
this.isCompleted = false,
});
}
class TodoApp extends StatelessWidget {
const TodoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'To-Do List',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
),
useMaterial3: true,
),
home: const TodoPage(),
);
}
}
class TodoPage extends StatefulWidget {
const TodoPage({super.key});
@override
State createState() => _TodoPageState();
}
class _TodoPageState extends State {
final List _todos = [];
final TextEditingController _controller =
TextEditingController();
void _addTodo() {
final text = _controller.text.trim();
if (text.isEmpty) {
return;
}
setState(() {
_todos.add(
Todo(title: text),
);
});
_controller.clear();
}
void _deleteTodo(int index) {
setState(() {
_todos.removeAt(index);
});
}
void _toggleTodo(int index, bool value) {
setState(() {
_todos[index].isCompleted = value;
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My To-Do List'),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Enter a task',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _addTodo(),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _addTodo,
child: const Text('Add'),
),
],
),
const SizedBox(height: 16),
Expanded(
child: _todos.isEmpty
? const Center(
child: Text(
'No tasks yet',
style: TextStyle(fontSize: 18),
),
)
: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
final todo = _todos[index];
return Card(
child: ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (value) {
_toggleTodo(
index,
value ?? false,
);
},
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
trailing: IconButton(
icon: const Icon(
Icons.delete,
),
onPressed: () {
_deleteTodo(index);
},
),
),
);
},
),
),
],
),
),
);
}
}
26. Understanding the Complete Code
| Code | Purpose |
|---|
| Todo | Represents a single task |
| _todos | Stores all tasks |
| _controller | Reads input from TextField |
| _addTodo() | Adds a new task |
| _deleteTodo() | Removes a task |
| _toggleTodo() | Changes task completion status |
| setState() | Requests a UI rebuild after state changes |
| ListView.builder | Builds the task list |
| Checkbox | Marks a task as complete or incomplete |
| dispose() | Releases the TextEditingController |
27. Widget Tree
MaterialApp
│
└── TodoPage
│
└── Scaffold
├── AppBar
│ └── Text
│
└── Body
└── Column
├── Row
│ ├── TextField
│ └── ElevatedButton
│
└── Expanded
└── ListView.builder
└── Card
└── ListTile
├── Checkbox
├── Text
└── IconButton
28. Empty Task State
It is good practice to show a useful message when there are no tasks.
_todos.isEmpty
? const Center(
child: Text('No tasks yet'),
)
: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
return Container();
},
)
29. Task Count
You can display the total number of tasks at the top of the screen.
Text(
'Total Tasks: ${_todos.length}',
)
You can also calculate completed tasks:
final completedCount =
_todos.where((todo) => todo.isCompleted).length;
30. Pending Task Count
final pendingCount =
_todos.where((todo) => !todo.isCompleted).length;
This can be used to display information such as:
Total Tasks: 5
Completed: 2
Pending: 3
31. Add Delete Confirmation
For a production application, you can ask the user for confirmation before deleting a task.
Future _confirmDelete(int index) async {
final result = await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Delete Task'),
content: const Text(
'Are you sure you want to delete this task?',
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context, false);
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(context, true);
},
child: const Text('Delete'),
),
],
);
},
);
if (result == true) {
setState(() {
_todos.removeAt(index);
});
}
}
32. Swipe to Delete
Flutter also provides Dismissible for interactions such as swiping a list item away. This can be used to implement swipe-to-delete functionality.
Dismissible(
key: ValueKey(todo),
onDismissed: (direction) {
setState(() {
_todos.removeAt(index);
});
},
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete),
),
child: ListTile(
title: Text(todo.title),
),
)
33. Adding a Task with the Keyboard
The onSubmitted callback allows the user to press the keyboard's action button to add a task.
TextField(
controller: _controller,
onSubmitted: (_) {
_addTodo();
},
)
34. Input Validation
Empty tasks should not be added to the list.
final text = _controller.text.trim();
if (text.isEmpty) {
return;
}
For more advanced applications, you can also limit task length or use a Form with validation.
35. Improving the UI
The basic application can be improved using Cards, rounded input fields, icons, spacing, themes, and better typography.
Card(
elevation: 2,
margin: const EdgeInsets.symmetric(
vertical: 6,
),
child: ListTile(
title: Text(todo.title),
),
)
36. Common Mistakes
- Forgetting to call setState() after changing the task list.
- Adding empty tasks.
- Not disposing TextEditingController.
- Using an incorrect list index when deleting an item.
- Creating unnecessary state variables.
- Not handling the empty-list state.
- Using a fixed-height list that causes overflow.
- Putting ListView inside Column without Expanded or another appropriate constraint.
37. Best Practices
- Keep task-related logic inside dedicated methods.
- Use meaningful variable and method names.
- Use a model class when task data becomes more complex.
- Validate user input.
- Dispose controllers when they are no longer needed.
- Use ListView.builder for dynamic lists.
- Keep UI code readable and organized.
- Use reusable widgets when individual task items become complex.
- Separate data, business logic, and UI as the application grows.
38. Real-World Features You Can Add
| Feature | Description |
|---|
| Task Categories | Group tasks such as Work, Study, Personal, and Shopping. |
| Due Dates | Allow users to select a deadline. |
| Priority | Set Low, Medium, or High priority. |
| Search | Search tasks by title. |
| Filter | Show All, Pending, or Completed tasks. |
| Dark Mode | Allow users to switch between light and dark themes. |
| Notifications | Remind users about upcoming tasks. |
| Local Storage | Save tasks so they remain after restarting the app. |
| Cloud Database | Synchronize tasks between devices. |
| Authentication | Allow each user to manage their own tasks. |
39. Local Data Storage
The basic application stores tasks only in memory. When the application is closed, the list is lost.
For permanent storage, you can integrate a local database or storage solution. This allows tasks to remain available after restarting the application.
Temporary state
↓
App running
↓
Tasks stored in memory
↓
App closed
↓
Data can be lost
Persistent storage
↓
App running
↓
Tasks saved to storage
↓
App closed
↓
App reopened
↓
Tasks can be loaded again
40. Firebase Integration
A more advanced To-Do application can use Firebase to store tasks online. A Firebase-based application can support authentication, cloud data storage, synchronization, and other backend features.
User
↓
Flutter To-Do App
↓
Firebase Authentication
↓
Cloud Database
↓
User Tasks
41. Search Functionality
A search field can be added to filter tasks based on the entered keyword.
final filteredTodos = _todos.where((todo) {
return todo.title
.toLowerCase()
.contains(searchText.toLowerCase());
}).toList();
42. Filtering Tasks
A To-Do app can provide filters such as:
- All Tasks
- Pending Tasks
- Completed Tasks
final completedTodos = _todos
.where((todo) => todo.isCompleted)
.toList();
final pendingTodos = _todos
.where((todo) => !todo.isCompleted)
.toList();
43. Application Testing
Test the application by performing the following actions:
- Launch the application.
- Verify that the empty state is displayed.
- Enter a task.
- Press Add.
- Verify that the task appears.
- Add multiple tasks.
- Mark a task as completed.
- Verify the line-through styling.
- Uncheck the completed task.
- Delete a task.
- Try adding an empty task.
- Restart the application and verify the expected storage behavior.
44. Practical Project Flow
Start
↓
Create Flutter Project
↓
Create Todo Model
↓
Create Stateful Todo Page
↓
Create TextField
↓
Create Add Button
↓
Create _addTodo()
↓
Store Tasks in List
↓
Display Tasks with ListView.builder
↓
Add Checkbox
↓
Add Delete Button
↓
Add Validation
↓
Test Application
↓
Improve UI
↓
Add Persistent Storage
↓
Complete To-Do App
45. Interview Questions
Q1. Why is StatefulWidget used in a To-Do List?
Because the task list and task completion status can change while the application is running.
Q2. What does setState() do?
setState() tells Flutter that the State object has changed and the framework should rebuild the relevant UI.
Q3. Why use TextEditingController?
It provides access to and control over the text entered into a TextField.
Q4. Why use ListView.builder?
It is useful for displaying dynamic lists because list items are built as needed.
Q5. How do you delete a task?
Use the list's removeAt() method with the appropriate index and call setState() so the UI reflects the change.
Q6. How can you mark a task as completed?
Store a Boolean value such as isCompleted in the Todo model and update it when the Checkbox changes.
Q7. Why should TextEditingController be disposed?
Controllers hold resources and should be disposed when the associated State object is removed from the widget tree.
Q8. How can tasks survive an application restart?
Use persistent storage such as a local database/storage solution or a cloud backend.
46. Mini Project Enhancements
After completing the basic To-Do List, try implementing the following features yourself:
- Add task editing.
- Add task priority.
- Add due dates.
- Add categories.
- Add search.
- Add task filters.
- Add dark mode.
- Add confirmation before deletion.
- Add swipe-to-delete.
- Add local persistence.
- Add Firebase authentication.
- Add cloud synchronization.
47. Learning Outcome
Building this To-Do List App gives practical experience with some of the most important Flutter concepts. You learn how user input is collected, how data is stored in memory, how widgets respond to user actions, how lists are rendered, and how state changes are reflected in the UI.
The project also provides a foundation for larger applications such as task management systems, shopping lists, habit trackers, notes applications, reminders, and productivity applications.
48. Summary
- A To-Do List is a practical Flutter beginner project.
- StatefulWidget is useful because the task data changes during runtime.
- TextEditingController reads user input.
- List stores task objects.
- ListView.builder displays dynamic tasks.
- Checkbox changes task completion status.
- setState() updates the UI after state changes.
- removeAt() can be used to delete tasks.
- dispose() should be used for the TextEditingController.
- Persistent storage can be added for long-term task storage.
- Firebase can be added for authentication and cloud synchronization.
49. Learn More About Flutter
JustAcademy Flutter Training Course
Register for Flutter Course Demo