Build a To-Do List App with Flutter

Build a Flutter to-do list app in Dart that runs live in Chrome.

Introduction

30 Second Summary

Everyone has a to-do list somewhere. Maybe it is scribbled on a sticky note, buried in a phone app, or floating around in your head. Building your own to-do app from scratch is one of the best ways to learn how a modern app framework actually works.

In this project, you will build a fully functional to-do list app using Flutter and Dart. You will type tasks into a text field, add them to a scrollable list, and tap them to mark as complete.

What You'll Build

A to-do list app that runs in your Chrome browser, built with Flutter's widget system and styled with Material 3 design.

By the end of this project, you'll have:

  • A working Flutter project with a clean file structure running in Chrome.
  • A StatefulWidget-based to-do screen where you can add tasks and tap them to mark as complete.
  • Hands-on experience with Dart syntax, the setState pattern, and Flutter's instant hot reload development loop.
  • Secret Mission: Add swipe-to-delete with a red background indicator and a confirmation message.

Are there any prerequisites?

You should be comfortable with basic programming concepts like variables, functions, and classes in any language. No prior Flutter or Dart experience is needed.

Before We Start

Before We Start...

Install Flutter and Set Up VS Code

Before you can build a Flutter app, you need a few tools installed on your machine.

Flutter requires Git to download and manage its SDK. In this step, you will install Git for Windows, set up the Flutter SDK through the VS Code extension, and verify everything is ready to go.

In this step, get ready to:
  • Install Git for Windows.
  • Install the Flutter SDK via the VS Code Flutter extension.
  • Verify your setup with flutter doctor.
Install Git for Windows

Flutter uses Git behind the scenes to download and update its SDK. You need Git installed before Flutter can set itself up.

  • Check if Git is already installed by opening your VS Code terminal (Ctrl+`) and running this command:
git --version

✔️ I see a version number

If you see output like git version 2.x.x, Git is already installed. Skip ahead to the next substep.

ⓧ Command not found

Git is not installed yet. Follow these steps to install it.

  • Open your browser and go to https://git-scm.com.
  • Click the download link for Windows. The installer will begin downloading automatically.
  • Run the downloaded installer.
  • Accept the default settings on each screen by clicking Next until you reach the end.
  • Click Install, then click Finish when the installation completes.
  • Close and reopen your VS Code terminal so the new PATH takes effect.
  • Confirm Git is installed by running:
git --version

You should now see a version number like git version 2.54.0.

Still not seeing a version number?

Make sure you closed and reopened your terminal after installing. The old terminal session does not know about the new PATH entry.

If it still does not work, restart VS Code entirely and try again.

Help me troubleshoot my Git installation on Windows.

Install Flutter via VS Code

The recommended way to install Flutter on Windows is through the VS Code extension. It handles the SDK download, cloning, and PATH setup all in one flow.

  • First, check if Flutter is already on your machine by running this command in your VS Code terminal:
flutter --version

✔️ I see Flutter 3.44 or higher

If the output shows Flutter 3.44.0 or higher, you already have the right version. Skip ahead to the verification substep below.

ⓧ I see an older version

You have Flutter installed, but it is an older version. Update it to the latest stable release.

  • Upgrade Flutter to the latest stable version by running this command:
flutter upgrade

This downloads the latest Flutter SDK on your current channel. When it finishes, you should see Flutter 3.44.0 (or higher) in the output.

Upgrade stuck or failing?

Make sure your internet connection is stable. The upgrade downloads several hundred megabytes.

If you see a channel error, run flutter channel stable first, then run flutter upgrade again.

Help me fix my Flutter upgrade issue.

ⓧ Command not found

Flutter is not installed yet. Follow these steps to install it through VS Code.

  • In VS Code, open the Extensions panel by pressing Ctrl+Shift+X.
  • Type Flutter in the search bar.
  • Find the extension named Flutter (by Dart Code) and click Install.
  • Open the Command Palette by pressing Ctrl+Shift+P.
  • Type Flutter: New Project and select it from the dropdown.
  • When prompted to locate or download the SDK, click Download SDK.
  • Choose a folder for the SDK installation. Use C:\dev as the destination.
  • Click Clone Flutter and wait for the download to complete. This may take a few minutes depending on your internet speed.
  • When the download finishes, click Add SDK to PATH when prompted.
  • Close all open terminals in VS Code by clicking the trash icon on each terminal tab.
  • Restart VS Code entirely (close the window and reopen it).

Why do I need to restart VS Code?

Adding the Flutter SDK to your PATH only takes effect in new terminal sessions. Restarting VS Code ensures every new terminal you open can find the flutter command.

Don't see the Download SDK option?

Make sure you installed the Flutter extension (not just the Dart extension). Check your Extensions panel for the green Flutter icon.

If the Command Palette does not show Flutter: New Project, try reloading VS Code with Ctrl+Shift+P then typing Developer: Reload Window.

Help me install the Flutter SDK through VS Code.

Verify your installation

Now that Flutter is installed, confirm that both Flutter and Chrome are detected correctly.

  • Open a new terminal in VS Code by pressing Ctrl+`.
  • Run the Flutter doctor command to check your environment:
flutter doctor -v

What does this command do?

The flutter doctor -v command scans your system for all the tools Flutter needs. The -v flag gives you verbose output so you can see exactly what is installed and what version each tool is.

  • Look at the output and confirm you see a checkmark next to Flutter (showing version 3.44.0 or higher).
  • Confirm you see a checkmark next to Chrome (showing it is available for web development).

Do I need to fix every issue flutter doctor shows?

No. For this project you only need Flutter and Chrome to have checkmarks. You may see warnings about Android toolchain or other platforms. Those are safe to ignore because we are building for the web only.

Flutter or Chrome not showing a checkmark?

If Flutter is not detected, make sure you restarted VS Code after adding the SDK to PATH. Open a fresh terminal and try again.

If Chrome is not detected, make sure Google Chrome is installed and accessible. Flutter looks for Chrome in its default install location.

Help me fix my flutter doctor output.

Your development environment is ready. Next up, you will create your Flutter project and see it running in Chrome.

Create Your Flutter Project

Your development environment is ready. Flutter is installed, VS Code has the right extensions, and Chrome is standing by as your test browser.

Now it's time to create the project you'll build for the rest of this walkthrough. Flutter gives you a fully working starter app out of the box, so you can see the development loop in action before writing your own code.

In this step, get ready to:
  • Create a new Flutter project called todo_app.
  • Explore the generated project structure.
  • Run the default app in Chrome with hot reload.
Create the todo_app project

Flutter's flutter create command scaffolds a complete project with all the files you need to build for web, Android, and iOS.

  • In the VS Code terminal from earlier, navigate to your development folder and create the project by running these commands:
cd C:\dev
flutter create todo_app

What do these commands do?

  • cd C:\dev moves into the folder where you installed Flutter.
  • flutter create todo_app generates a new Flutter project with the name todo_app. Flutter uses lowercase_with_underscores for project names.

You should see a list of generated files scroll by, ending with "All done!" in the terminal output.

Seeing 'flutter' is not recognized?

This means the Flutter SDK is not in your system PATH. Close and reopen your terminal, then try again. If it still fails, restart VS Code completely so the updated PATH takes effect.

Help me fix my Flutter PATH issue on Windows

  • Move into the new project folder and open it in a fresh VS Code window by running:
cd todo_app
code .

A new VS Code window opens with todo_app as the workspace. Switch to this new window for the rest of the project.

Explore the project structure

Before writing any code, take a moment to understand what Flutter generated for you. Every Flutter project follows the same folder layout.

  • In the Explorer sidebar (left panel of VS Code), look at the top-level files and folders.

What are the key files and folders?

  • lib/main.dart is your app's entry point. This is where you'll write all your Dart code.
  • pubspec.yaml is where dependencies and project metadata live. Think of it like a package.json if you've used Node.js.
  • web/ contains web-specific assets like index.html. This is what makes your app run in Chrome.
  • android/ and ios/ folders exist for mobile builds. You will not touch them in this project.
Run the default app in Chrome

Flutter generated a working counter app as a starting point. Running it now lets you confirm that your entire toolchain works before you make any changes.

  • Open a new terminal in the VS Code window for todo_app by pressing Ctrl+`.
  • Launch the app in Chrome by running:
flutter run -d chrome

What does this command do?

flutter run -d chrome compiles your Dart code into JavaScript and opens it in a Chrome window. The -d chrome flag tells Flutter which device to target.

The first run takes a minute or two while Flutter compiles everything. Subsequent runs are much faster thanks to hot reload.

The terminal will show a message saying to press "r" for hot reload once the app is ready.

A Chrome window opens with the Flutter demo app. You should see a counter at "0" in the center and a blue floating action button (the "+" icon) in the bottom-right corner.

  • Tap the + button a few times to confirm the counter increments.
  • Back in the VS Code terminal, press r to trigger a hot reload. Nothing visible changes yet since you haven't edited code, but this confirms the reload loop works.
  • When you're done exploring, press q in the terminal to quit the running app.

Chrome didn't open?

Make sure Chrome is installed and detected. Run flutter doctor -v and check the Chrome row. If it says "Cannot find Chrome", verify Chrome is installed at its default location.

If you see a "port already in use" error, close any other Flutter processes or Chrome DevTools tabs, then try again.

Help me troubleshoot Chrome not opening with flutter run

Your Flutter project is scaffolded and running. Next up, you'll replace this default counter app with your own to-do list UI.

Build the To-Do App UI

In the previous step, you created a Flutter project and ran the default counter app in Chrome. That app was a helpful proof that everything works, but now it is time to replace it with your own code.

Every Flutter app is built from widgets. Widgets are like building blocks that describe what the UI should look like. In this step, you will replace the default app with a to-do list shell that has an app bar, a list area, a text input, and an add button.

In this step, get ready to:
  • Replace the default counter app with a clean app shell.
  • Build the TodoScreen layout with an AppBar, list area, and input row.
  • Wire up a TextEditingController to the text field.
Replace the default counter app

The default counter app has a lot of code you do not need. You will select all of it and replace it with a clean structure designed for your to-do list.

  • In VS Code, open lib/main.dart from the Explorer sidebar.
  • Select all the code with Ctrl+A and delete it.
  • Replace it with the following code:
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My Todos',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const TodoScreen(),
    );
  }
}

class TodoScreen extends StatefulWidget {
  const TodoScreen({super.key});

  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Todos'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: 0,
              itemBuilder: (context, index) {
                return const SizedBox();
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Enter a new todo',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                FloatingActionButton(
                  onPressed: () {},
                  child: const Icon(Icons.add),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

What does this code do?

This file has three main sections:

  • MyApp is a StatelessWidget that sets up the overall app. It configures Material 3 theming with a deep purple color scheme and points to TodoScreen as the home page.
  • TodoScreen is a StatefulWidget. Unlike a StatelessWidget, it can hold and change data over time. Your to-do list needs this because items get added and toggled.
  • _TodoScreenState contains the actual UI layout. The build() method returns a Scaffold with an AppBar, a list area, and an input row at the bottom.
Understand the TodoScreen layout

The layout inside _TodoScreenState uses a pattern you will see in many Flutter apps. A Column stacks children vertically, and Expanded tells a widget to take up all remaining space.

How is the layout structured?

  • Scaffold provides the basic app structure (app bar, body area).
  • Column arranges the list and the input row vertically.
  • Expanded > ListView.builder creates a scrollable list that takes all available space above the input row. Right now itemCount: 0 means it renders nothing.
  • Padding > Row holds a TextField (for typing) and a FloatingActionButton (for adding). The button's onPressed is empty for now.
Understand the TextEditingController

Near the top of _TodoScreenState, you declared a TextEditingController. This object connects your code to the text field so you can read what the user typed and clear the field after adding a task.

Why call dispose()?

A TextEditingController allocates memory to track the text field's value. The dispose() method releases that memory when the screen is removed. Forgetting to dispose causes memory leaks.

This is a common Flutter pattern. Any controller you create should be disposed in the dispose() lifecycle method.

  • Save lib/main.dart.
  • Switch to your terminal where flutter run is active and press r to hot reload.
  • In Chrome, you should see a purple AppBar titled My Todos, an empty area below it, and a text input field with a round purple add button at the bottom.

Seeing a red error screen in Chrome?

  • Check that you deleted ALL the old counter-app code before pasting. Any leftover class definitions will conflict with the new code.
  • Look for red squiggly lines in VS Code. They indicate syntax errors like a missing comma, bracket, or semicolon.
  • Make sure the import line at the very top reads import 'package:flutter/material.dart'; exactly.

Why is my Flutter app showing a red error screen?

✔️ Awesome, I've got everything!

Great. Double check you have saved lib/main.dart before moving on.

ⓧ I'd like to double check the full code

Here is the complete lib/main.dart file you should have at this point:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My Todos',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const TodoScreen(),
    );
  }
}

class TodoScreen extends StatefulWidget {
  const TodoScreen({super.key});

  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Todos'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: 0,
              itemBuilder: (context, index) {
                return const SizedBox();
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Enter a new todo',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                FloatingActionButton(
                  onPressed: () {},
                  child: const Icon(Icons.add),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Your app shell is ready. It looks clean but does not do anything yet. In the next step, you will add the data model and the logic that makes adding and completing to-dos actually work.

Add To-Do Items and Manage State

In the previous step, you built the visual shell of your to-do app. It looks like a to-do list, but nothing happens when you type or tap. The UI is static.

In this step, you will bring the app to life with state management. You will create a data model for to-do items, store them in a list, and use Flutter's setState to trigger UI rebuilds whenever data changes.

In this step, get ready to:
  • Create a data model class to represent each to-do item.
  • Add functionality to create new to-dos and store them in state.
  • Display to-dos in a dynamic list with tap-to-complete toggling.
Create the todo data model

Every to-do item needs a consistent shape. A title, a completion status, and a unique ID so Flutter can tell items apart.

  • In lib/main.dart, find the closing brace of the MyApp class.
  • Add the following TodoItem class below it (above the TodoScreen class):
class TodoItem {
  final String id;
  final String title;
  bool isDone;

  TodoItem({
    required this.title,
    this.isDone = false,
  }) : id = DateTime.now().millisecondsSinceEpoch.toString();
}

What does this code do?

This class is the blueprint for every to-do item in your app.

  • id is generated automatically from the current timestamp. This gives each item a unique identifier Flutter uses to track it.
  • title is the text the user types in. It is marked required because every to-do needs a name.
  • isDone defaults to false so new items start uncompleted.
  • Save lib/main.dart.
  • Press r in your terminal to hot reload. You will not see a visible change yet because the class is defined but not used.

Seeing a red error banner in Chrome?

  • Check that the TodoItem class is placed between the closing brace of MyApp and the class TodoScreen line. It should not be inside another class.
  • Make sure the class has a closing brace } at the end. Mismatched braces are the most common Dart syntax error.

Still stuck? Help me fix a Dart class syntax error in my Flutter todo app

Implement the add functionality

With the data model ready, you need a place to store to-do items and a method to create them. In Flutter, calling setState() tells the framework to rebuild the UI with the updated data.

  • Inside _TodoScreenState, find this line:
  final TextEditingController _controller = TextEditingController();
  • Add a _todos list directly above it, then add the _addTodo() method after the dispose() method. Copy this code:
  final List<TodoItem> _todos = [];
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _addTodo() {
    // Read the text from the input field and trim whitespace
    final text = _controller.text.trim();
    // Do nothing if the input is empty
    if (text.isEmpty) return;

    // Tell Flutter to rebuild the UI with the new item
    setState(() {
      _todos.add(TodoItem(title: text));
    });
    // Clear the input field for the next entry
    _controller.clear();
  }

What does this code do?

  • _todos is an empty list that will hold all your TodoItem objects.
  • _addTodo() reads whatever the user typed, checks it is not blank, then wraps the list-add operation inside setState(). The setState call is what triggers Flutter to re-render the screen with the new data.
  • _controller.clear() empties the text field after adding, so the user can immediately type the next task.

What is setState?

setState is Flutter's way of saying "my data changed, please rebuild the UI." Any change to state that should appear on screen must happen inside a setState() call. Without it, your list would update in memory but the screen would stay frozen.

Now wire the _addTodo method to the button and text field so the user can trigger it.

  • Scroll down to the FloatingActionButton in your build method. Find this line:
                  onPressed: () {},
  • Replace () {} with _addTodo so it looks like this:
                  onPressed: _addTodo,
  • Now find the TextField widget above the button. Find the line with border: OutlineInputBorder(), and add an onSubmitted parameter after the closing parenthesis of InputDecoration. The TextField should now look like this:
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Enter a new todo',
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (_) => _addTodo(),
                  ),

Why the underscore in onSubmitted?

onSubmitted passes the text field's value as a parameter. The _ means "I receive this parameter but do not need it" because _addTodo() already reads from the controller directly.

  • Save lib/main.dart and press r in your terminal to hot reload.
  • Type something in the text field and press **Enter** or tap the add button. Nothing visible appears in the list yet (because the ListView still has itemCount: 0), but the text field should clear. That confirms _addTodo() is running.

Text field not clearing after pressing Enter?

  • Make sure onSubmitted: (_) => _addTodo() is inside the TextField widget, not inside InputDecoration.
  • Check that onPressed: _addTodo has no parentheses after _addTodo. You pass the function reference, not call it.

Need more help? Help me wire _addTodo to my TextField and FloatingActionButton in Flutter

Display todos in the ListView

Items are being added to the list in memory, but the screen does not reflect that yet. You need to update the ListView.builder to read from _todos and render each item as a tappable tile.

  • First, add a _toggleTodo method directly below _addTodo(). Copy this code:
  void _toggleTodo(int index) {
    setState(() {
      _todos[index].isDone = !_todos[index].isDone;
    });
  }
  • Now find the entire Expanded widget inside the Column children. Replace it (from Expanded( through its closing ),) with this code:
          Expanded(
            child: _todos.isEmpty
                ? const Center(
                    child: Text(
                      'No todos yet. Add one below!',
                      style: TextStyle(fontSize: 16, color: Colors.grey),
                    ),
                  )
                : ListView.builder(
                    itemCount: _todos.length,
                    itemBuilder: (context, index) {
                      final todo = _todos[index];
                      return ListTile(
                        title: Text(
                          todo.title,
                          style: TextStyle(
                            // Strike through completed items
                            decoration: todo.isDone
                                ? TextDecoration.lineThrough
                                : TextDecoration.none,
                            color: todo.isDone ? Colors.grey : Colors.black,
                          ),
                        ),
                        // Checkbox icon changes based on completion
                        leading: Icon(
                          todo.isDone
                              ? Icons.check_box
                              : Icons.check_box_outline_blank,
                          color: todo.isDone
                              ? Colors.green
                              : Colors.grey,
                        ),
                        // Tap to toggle completion
                        onTap: () => _toggleTodo(index),
                      );
                    },
                  ),
          ),

What does this code do?

  • _toggleTodo() flips a to-do's isDone value inside setState(), which tells Flutter to rebuild and show the updated styling.
  • The ternary _todos.isEmpty ? ... : ... shows a friendly placeholder message when the list is empty. Once items exist, it displays the ListView.builder.
  • Each ListTile renders a to-do with a checkbox icon on the left and the title text. When isDone is true, the text gets a strikethrough and turns grey.
  • onTap calls _toggleTodo(index) so tapping any item marks it as complete or incomplete.
  • Save lib/main.dart and press r in your terminal to hot reload.
  • You should see the message "No todos yet. Add one below!" in the center of the screen.
  • Type a task (e.g. Buy groceries) and press **Enter** or tap the add button. The task appears in the list with a grey checkbox.
  • Tap the task. The text gets a strikethrough and the checkbox turns green.
  • Tap it again to toggle it back to incomplete.

Items not showing up in the list?

  • Make sure you replaced the entire old Expanded(...) block. If both the old and new code exist, you will see a compile error.
  • Verify that itemCount: _todos.length is present, not the old itemCount: 0.
  • Check that onPressed: _addTodo is on the FloatingActionButton. If the old empty () {} is still there, items are never being added.

Still stuck? Help me debug why my Flutter ListView is not displaying to-do items

✔️ Awesome, I've got everything!

Great. Double check you have saved lib/main.dart before moving on.

ⓧ I'd like to double check the full code

Here is the complete lib/main.dart at the end of this step:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My Todos',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const TodoScreen(),
    );
  }
}

class TodoItem {
  final String id;
  final String title;
  bool isDone;

  TodoItem({
    required this.title,
    this.isDone = false,
  }) : id = DateTime.now().millisecondsSinceEpoch.toString();
}

class TodoScreen extends StatefulWidget {
  const TodoScreen({super.key});

  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  final List<TodoItem> _todos = [];
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _addTodo() {
    final text = _controller.text.trim();
    if (text.isEmpty) return;

    setState(() {
      _todos.add(TodoItem(title: text));
    });
    _controller.clear();
  }

  void _toggleTodo(int index) {
    setState(() {
      _todos[index].isDone = !_todos[index].isDone;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Todos'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Column(
        children: [
          Expanded(
            child: _todos.isEmpty
                ? const Center(
                    child: Text(
                      'No todos yet. Add one below!',
                      style: TextStyle(fontSize: 16, color: Colors.grey),
                    ),
                  )
                : ListView.builder(
                    itemCount: _todos.length,
                    itemBuilder: (context, index) {
                      final todo = _todos[index];
                      return ListTile(
                        title: Text(
                          todo.title,
                          style: TextStyle(
                            decoration: todo.isDone
                                ? TextDecoration.lineThrough
                                : TextDecoration.none,
                            color: todo.isDone ? Colors.grey : Colors.black,
                          ),
                        ),
                        leading: Icon(
                          todo.isDone
                              ? Icons.check_box
                              : Icons.check_box_outline_blank,
                          color: todo.isDone
                              ? Colors.green
                              : Colors.grey,
                        ),
                        onTap: () => _toggleTodo(index),
                      );
                    },
                  ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Enter a new todo',
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (_) => _addTodo(),
                  ),
                ),
                const SizedBox(width: 8),
                FloatingActionButton(
                  onPressed: _addTodo,
                  child: const Icon(Icons.add),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Your to-do app now creates, displays, and toggles tasks. You have seen how setState() drives every UI update in Flutter. Next up, you will take this further in the Secret Mission by adding swipe-to-delete with animations and user feedback.

Secret mission

Add Swipe-to-Delete with Dismissible

Your to-do app lets you add and complete tasks. But what about removing them entirely? In this secret mission, you will add swipe-to-delete functionality using Flutter's Dismissible widget, complete with a red background indicator and a SnackBar confirmation.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources installed, pause them to come back later, or delete them entirely.

Resources you used:

  • todo_app project folder.
  • Flutter SDK (installed to your chosen directory, e.g. C:\dev\flutter).
  • Flutter VS Code extension.
  • Git for Windows.

Keep everything running

No action needed. Choose this if you plan to keep building Flutter apps or want to revisit this project later. Your Flutter SDK, VS Code extension, and project files will remain in place.

  • Your todo_app folder stays where you created it.
  • The Flutter SDK stays installed and available from your terminal.
  • Git for Windows remains available for other projects.

Pause - I'll come back to this later

Since this project is fully local, there is nothing to pause. Your files and tools will stay on disk until you decide to use or remove them.

  • If you have a flutter run session still active, press q in the terminal to quit it.
  • Your project files and Flutter SDK remain ready for next time.

Delete - I don't want to use this again

Remove all project files, the Flutter SDK, and tools you installed during this project. Follow these steps to clean up completely.

  • If you have a flutter run session active, press q in the terminal to stop it.
  • Delete the todo_app project folder by running this in PowerShell:
Remove-Item -Recurse -Force -Path "C:\dev\todo_app"
  • Delete the Flutter SDK folder by running:
Remove-Item -Recurse -Force -Path "C:\dev\flutter"
  • Optionally, remove Flutter and Dart configuration files by running:
Remove-Item -Recurse -Force -Path (Join-Path $env:APPDATA ".flutter-devtools")
Remove-Item -Recurse -Force -Path (Join-Path $env:APPDATA ".dart"), (Join-Path $env:APPDATA ".dart-tool"), (Join-Path $env:LOCALAPPDATA ".dartServer")
Remove-Item -Recurse -Force -Path (Join-Path $env:LOCALAPPDATA "Pub\Cache")
  • Remove Flutter from your system PATH. Press the Windows key, type Environment Variables, and press Enter.
  • Under User variables, select Path and click Edit.
  • Find the entry pointing to your Flutter SDK bin folder, select it, and click Delete. Click OK to save.
  • Uninstall the Flutter VS Code extension. In VS Code, open the Extensions panel (Ctrl+Shift+X), find Flutter, and click Uninstall.
  • Uninstall Git for Windows. Open Settings, go to Apps, then Installed apps. Search for Git, click the three-dot menu, and select Uninstall.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully functional to-do list app with Flutter and Dart, running live in your Chrome browser.

You've learned how to:

  • Installed the Flutter SDK and built a complete to-do app from scratch using Dart.
  • Used setState to trigger UI rebuilds, learning the core pattern that drives every StatefulWidget in Flutter.
  • Built a dynamic, scrollable list with ListView.builder and handled user input with TextEditingController.
  • Secret Mission: Added swipe-to-delete with the Dismissible widget, a red background indicator, and SnackBar feedback.

Ready to quiz yourself?