Marking tasks as done

A read-only tasks app is not very practical! Let’s add the ability to update the completed state and mark/unmark our tasks as done.

First, though, we’ll extract the tasks-specific code to a separate screen named TasksScreen:

class TasksScreen extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.tasks.watchAll();
    if (state.isLoading) {
      return CircularProgressIndicator();
    }
    return ListView(
      children: [
        for (final task in state.model!) Text(task.title),
      ],
    );
  }
}

Remember to return this new widget from TasksApp:

class TasksApp extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: ref.watch(repositoryInitializerProvider).when(
                error: (error, _) => Text(error.toString()),
                loading: () => const CircularProgressIndicator(),
                data: (_) => TasksScreen(),
              ),
        ),
      ),
      debugShowCheckedModeBanner: false,
    );
  }
}

Back to our TasksScreen we are going to wrap our title text widget in a ListTile prefixing it with a checkbox which, upon clicking, will toggle task completion:

class TasksScreen extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.tasks.watchAll();
    if (state.isLoading) {
      return CircularProgressIndicator();
    }
    return ListView(
      children: [
        for (final task in state.model!)
          ListTile(
            leading: Checkbox(
              value: task.completed,
              onChanged: (value) => task.toggleCompleted().save(),
            ),
            title: Text('${task.title} [id: ${task.id}]'),
          ),
      ],
    );
  }
}

If only the toggleCompleted() method existed… 😀

Since Task is immutable, we return a new Task object with the inverse boolean value of completed:

@JsonSerializable()
@DataRepository([JsonServerAdapter])
class Task extends DataModel<Task> {
  @override
  final int? id;
  final String title;
  final bool completed;

  Task({this.id, required this.title, this.completed = false});

  Task toggleCompleted() {
    return Task(id: this.id, title: this.title, completed: !this.completed).withKeyOf(this);
  }
}

What exactly is withKeyOf(this) for?

When a new model is created, Flutter Data initializes it looking up its internal key based on its id.

This way, Task(id: 4, title: 'a') and Task(id: 4, title: 'b') are essentially two versions of the same model.

When there is no id (or potentially no id, as in the case above) we can use withKeyOf to ensure the new model is treated as an updated version of the old one.

Regenerate code, hot-reload and check all boxes…

Done! NEXT: Creating a new task