Creating a Todo List app (CRUD) with Flutter and Isar DB

As you may know, Flutter is a framework for creating cross-platform apps. Isar DB is one of the best database engines you can use with Flutter. You can see directly on the website why you should use Isar DB for your next project.

Why use isar DB

In this article, we will create a simple Todo List app using Flutter and Isar DB. The todo list app will enable the user to insert todo, display it, set it as finished, and remove it.

Preparation

The first thing, we are gonna do is create a new project. We will name it todoapp like so:

flutter create todoapp

Next, let's add some packages to the repository

cd todoapp
flutter pub add isar isar_flutter_libs path_provider provider
flutter pub add -d isar_generator build_runner

The command above will add some packages. There are main packages and development packages. We use -d flag to indicate that we are installing packages for development purposes.

Building the model

After installing all those packages, let's create our new model. Inside the lib directory create a new folder called models, and a file named todo.dart in it.

import 'package:isar/isar.dart';

part 'todo.g.dart';

@Collection()
class Todo {
  Id id = Isar.autoIncrement;
  late String title;
  late bool isFinished;
  DateTime? createdDate;
  DateTime? updatedDate;
}

Then, open the terminal and run this command to generate the model:

dart run build_runner build

A new file named todo.g.dart will be generated in the same folder.

Preparing the provider

As you may guess, we will use provider as the state management. Let's create our todo provider. Inside the lib directory create a new folder called providers, then create todo_provider.dart in it.

import 'package:flutter/material.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import 'package:todoapptest/models/todo.dart';

class TodoProvider extends ChangeNotifier {
  TodoProvider() {
    db = openDB();
    init();
  }

  late Future<Isar?> db;

  List<Todo> _todos = [];
  List<Todo> get todos => _todos;

  void init() async {
    final isar = await db;
    isar!.txn(() async {
      final todoCollection = isar.todos;
      _todos = await todoCollection.where().findAll();
      notifyListeners();
    });
  }

  Future<Isar?> openDB() async {
    if (Isar.instanceNames.isEmpty) {
      final dir = await getApplicationDocumentsDirectory();
      return await Isar.open(
        [TodoSchema],
        directory: dir.path,
        inspector: true,
      );
    }

    return Isar.getInstance();
  }

  void addTodo(Todo todo) async {
    final isar = await db;
    await isar!.writeTxn(() async {
      await isar.todos.put(todo);
      _todos.add(todo);
      notifyListeners();
    });
  }

  void toggleFinished(Todo todo) async {
    final isar = await db;

    isar!.writeTxn(() async {
      todo.isFinished = !todo.isFinished;
      todo.updatedDate = DateTime.now();
      await isar.todos.put(todo);

      int todoIndex = _todos.indexWhere((element) => todo.id == element.id);
      _todos[todoIndex].isFinished = todo.isFinished;
      _todos[todoIndex].updatedDate = todo.updatedDate;
      notifyListeners();
    });
  }

  void deleteTodo(Todo todo) async {
    final isar = await db;
    await isar!.writeTxn(() async {
      await isar.todos.delete(todo.id);
      _todos.remove(todo);

      notifyListeners();
    });
  }
}
providers/todo_provider.dart

Building the UI

Let's start with our main.dart. We will wrap the MaterialApp widget with ChangeNotifierProvider so we can listen to the TodoProvider.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todoapptest/providers/todo_provider.dart';
import 'package:todoapptest/screens/todo_list.dart';

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => TodoProvider(),
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
          useMaterial3: true,
        ),
        home: TodoList(),
      ),
    );
  }
}
main.dart

We also created the screens folder with todo_list.dart file in it.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todoapptest/models/todo.dart';
import 'package:todoapptest/providers/todo_provider.dart';
import 'package:todoapptest/widgets/todo_item.dart';

class TodoList extends StatelessWidget {
  TodoList({super.key});

  final TextEditingController titleController = TextEditingController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Todo List"),
      ),
      body: Container(
        color: Colors.black12,
        child: Consumer<TodoProvider>(
          builder: (context, value, child) {
            return value.todos.length > 0
                ? ListView.builder(
                    padding: EdgeInsets.all(20),
                    itemCount: value.todos.length,
                    itemBuilder: (context, index) {
                      return TodoItem(value.todos.reversed.toList()[index]);
                    })
                : Center(
                    child: Text("Belum ada todo disini"),
                  );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          showDialog(
              context: context,
              builder: (context) {
                return AlertDialog(
                  title: const Text("Tambah Todo"),
                  content: SizedBox(
                    width: 300,
                    height: 150,
                    child: Column(
                      children: [
                        TextFormField(
                          controller: titleController,
                          decoration: const InputDecoration(
                            border: OutlineInputBorder(),
                            enabledBorder: OutlineInputBorder(
                                borderSide: BorderSide(color: Colors.grey)),
                            hintText: "Tulis todo-nya disini",
                            fillColor: Colors.white,
                            filled: true,
                          ),
                        ),
                        const SizedBox(
                          height: 30,
                        ),
                        ElevatedButton(
                            onPressed: () {
                              Todo todo = Todo()
                                ..title = titleController.text
                                ..isFinished = false
                                ..createdDate = DateTime.now();
                              Provider.of<TodoProvider>(context, listen: false)
                                  .addTodo(todo);

                              titleController.clear();
                              Navigator.pop(context);
                            },
                            style: const ButtonStyle(
                                backgroundColor:
                                    MaterialStatePropertyAll(Colors.blue)),
                            child: const Text(
                              "Simpan",
                              style: TextStyle(color: Colors.white),
                            ))
                      ],
                    ),
                  ),
                );
              });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}
screens/todo_list.dart

If you notice in the code above, we use the TodoItem widget to display a todo. Let's create the widget by creating a new directory widgets. Inside the widgets folder, create a file named todo_item.dart. Here is the content of todo_item.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todoapptest/models/todo.dart';
import 'package:todoapptest/providers/todo_provider.dart';

class TodoItem extends StatelessWidget {
  const TodoItem(this.todo, {super.key});
  final Todo todo;

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(bottom: 20),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(10),
      ),
      child: ListTile(
        leading: GestureDetector(
          onTap: () => Provider.of<TodoProvider>(context, listen: false)
              .toggleFinished(todo),
          child: Icon(
            todo.isFinished ? Icons.check_box : Icons.check_box_outline_blank,
            color: Colors.green,
          ),
        ),
        title: Text(
          todo.title,
          style: TextStyle(
              decoration: todo.isFinished ? TextDecoration.lineThrough : null),
        ),
        trailing: GestureDetector(
          onTap: () => Provider.of<TodoProvider>(context, listen: false)
              .deleteTodo(todo),
          child: Icon(Icons.delete),
        ),
      ),
    );
  }
}

That's it. Run the app using the flutter run command to see if it works.

Todo List app

Explanation

Inserting to-do

As you can see in the code above, the query-related code resides in todo_provider.dart. We add a todo to Isar in the addTodo function and call the function using

Provider.of<TodoProvider>(context, listen: false).addTodo(todo);

Not only inserting todo into DB, that function also updates the list and calls notifyListeners() to make the UI aware of the update in the DB. This ensures the UI to-do list stays up to date. If you look into the todo_list.dart you can see the code like the following:

Consumer<TodoProvider>(
  ...
)

That is how we listen to TodoProvider.

Updating to-do to Done

To update todo we use toggleFinished function, and call that function like so:

Provider.of<TodoProvider>(context, listen: false).toggleFinished(todo)

Since the toggleFinished function uses void as its return value, don't forget to set the listen parameter to false. Similar to add todo we also use notifyListener() to notify the listener of the provider.

Deleting to-do

It is obvious that we use the deleteTodo function to delete the todo. We call the function in similar ways that we call the other function. We also use the notifyListeners function.

Provider.of<TodoProvider>(context, listen: false).deleteTodo(todo)

You should be aware that the consumer function is listening to todos the property of TodoProvider. So, every time we do something with the data, we also update that property before calling the notifyListeners function.

Conclusion

In this article, We've learned to create a simple to-do list app with Flutter and Isar DB. From installing the necessary packages to using state management to interact with data and the UI. Hopefully, this example of how to build a to-do list app will be useful for you. Cheers.