Popular Searches
Popular Course Categories
Popular Courses

Build a Flutter To-Do List App

Build a Flutter To-Do List App

Flutter Practical Projects


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











TechnologyPurpose
FlutterUI framework for building the application
DartProgramming language used by Flutter
Material DesignProvides ready-made UI components
StatefulWidgetManages changing application state
TextFieldAccepts task input from the user
ListView.builderDisplays tasks efficiently in a list
CheckboxMarks 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:



  1. Reads the text from the TextField.

  2. Removes unnecessary spaces using trim().

  3. Checks whether the input is empty.

  4. Creates a Todo object.

  5. Adds the Todo object to the list.

  6. Calls setState() to update the UI.

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














CodePurpose
TodoRepresents a single task
_todosStores all tasks
_controllerReads 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.builderBuilds the task list
CheckboxMarks 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














FeatureDescription
Task CategoriesGroup tasks such as Work, Study, Personal, and Shopping.
Due DatesAllow users to select a deadline.
PrioritySet Low, Medium, or High priority.
SearchSearch tasks by title.
FilterShow All, Pending, or Completed tasks.
Dark ModeAllow users to switch between light and dark themes.
NotificationsRemind users about upcoming tasks.
Local StorageSave tasks so they remain after restarting the app.
Cloud DatabaseSynchronize tasks between devices.
AuthenticationAllow 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:



  1. Launch the application.

  2. Verify that the empty state is displayed.

  3. Enter a task.

  4. Press Add.

  5. Verify that the task appears.

  6. Add multiple tasks.

  7. Mark a task as completed.

  8. Verify the line-through styling.

  9. Uncheck the completed task.

  10. Delete a task.

  11. Try adding an empty task.

  12. 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:



  1. Add task editing.

  2. Add task priority.

  3. Add due dates.

  4. Add categories.

  5. Add search.

  6. Add task filters.

  7. Add dark mode.

  8. Add confirmation before deletion.

  9. Add swipe-to-delete.

  10. Add local persistence.

  11. Add Firebase authentication.

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


whatsapp