Menu
CoddyTech

To-do List: Empty States

Part of the Projects section of Coddy's Flutter journey. Lesson 9 of 36.

Tap Done before you finish anything and you get a blank screen. Is it broken, or is there nothing there? An empty state answers that: a quiet icon and a line that says what is going on.

Each filter has its own message, so a switch expression on _filter picks it:

String get _emptyMessage => switch (_filter) {
  Filter.all => 'No tasks yet',
  Filter.active => 'Nothing left to do',
  Filter.done => 'Nothing done yet',
};

Then the list's place shows one or the other: shown.isEmpty ? Center(...) : ListView.builder(...). Test the list that is shown, not _tasks: on Done, the full list is not empty but the screen is.

Give the icon the theme's outline color, Theme.of(context).colorScheme.outline. It is the soft grey of your seed color, so the empty state looks like part of the app.

This is the last lesson of the to-do list. The checks try the whole app: add, tick, swipe and filter.

challenge icon

Challenge

Medium

Show an empty state when shown is empty:

  • add an _emptyMessage getter: No tasks yet for All, Nothing left to do for Active, Nothing done yet for Done
  • in the list's place, show a centered Column of an Icons.task_alt icon (size 64, the theme's outline color), 12 pixels of space and the message
  • otherwise show the list as before

Try it yourself

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(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
      ),
      home: const TodoScreen(),
    );
  }
}

class Task {
  Task(this.title);

  final String title;
  bool done = false;
}

enum Filter { all, active, done }

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

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

class _TodoScreenState extends State<TodoScreen> {
  final List<Task> _tasks = [Task('Buy milk'), Task('Walk the dog'), Task('Read a book')];
  final _controller = TextEditingController();
  Filter _filter = Filter.all;

  int get _left => _tasks.where((task) => !task.done).length;

  List<Task> get _shown => switch (_filter) {
        Filter.all => _tasks,
        Filter.active => _tasks.where((task) => !task.done).toList(),
        Filter.done => _tasks.where((task) => task.done).toList(),
      };

  void _addTask() {
    final title = _controller.text.trim();
    if (title.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Type a task first')),
      );
      return;
    }
    setState(() {
      _tasks.add(Task(title));
    });
    _controller.clear();
  }

  void _deleteTask(Task task) {
    final index = _tasks.indexOf(task);
    setState(() {
      _tasks.remove(task);
    });
    ScaffoldMessenger.of(context).hideCurrentSnackBar();
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('Deleted ${task.title}'),
        action: SnackBarAction(
          label: 'Undo',
          onPressed: () {
            setState(() {
              _tasks.insert(index, task);
            });
          },
        ),
      ),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    final shown = _shown;
    return Scaffold(
      appBar: AppBar(title: const Text('My Tasks')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      labelText: 'New task',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                IconButton.filled(
                  onPressed: _addTask,
                  icon: const Icon(Icons.add),
                ),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: Row(
              children: [
                Text(
                  _left == 1 ? '1 task left' : '$_left tasks left',
                  style: Theme.of(context).textTheme.titleSmall,
                ),
              ],
            ),
          ),
          SegmentedButton<Filter>(
            segments: const [
              ButtonSegment(value: Filter.all, label: Text('All')),
              ButtonSegment(value: Filter.active, label: Text('Active')),
              ButtonSegment(value: Filter.done, label: Text('Done')),
            ],
            selected: {_filter},
            onSelectionChanged: (selection) {
              setState(() {
                _filter = selection.first;
              });
            },
          ),
          const SizedBox(height: 8),
          Expanded(
            child: ListView.builder(
              itemCount: shown.length,
              itemBuilder: (context, index) {
                final task = shown[index];
                return Dismissible(
                  key: ObjectKey(task),
                  direction: DismissDirection.endToStart,
                  background: Container(
                    color: Colors.red,
                    alignment: Alignment.centerRight,
                    padding: const EdgeInsets.symmetric(horizontal: 24),
                    child: const Icon(Icons.delete, color: Colors.white),
                  ),
                  onDismissed: (direction) => _deleteTask(task),
                  child: ListTile(
                    leading: Checkbox(
                      value: task.done,
                      onChanged: (value) {
                        setState(() {
                          task.done = value!;
                        });
                      },
                    ),
                    title: Text(
                      task.title,
                      style: task.done
                          ? const TextStyle(
                              decoration: TextDecoration.lineThrough,
                              color: Colors.grey,
                            )
                          : null,
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Projects

Practice on your own: Online Flutter compiler