Integrating multiple databases in a todos app (Personal project)
Tags: dart, cleancode, flutter
Published: 2024-06-25 00:35:12

**Project Overview**

- **Project Name:** cit_dr_todos_app
- **Duration:** 12 hrs 39 mins
- **Team Members:** Solo
- **Objective:** Learn how to integrate multiple databases in the same project

## **Goals and Objectives**

- **Original Goals:** Learn how to integrate multiple databases in the same project using clean architecture
- **Were the Goals Met?:**  Half of them, I was able to do the integration but I didn’t understand clean architecture at all, in fact the project feels really weird for me

## **Technical Overview**

- **Technologies Used:** Flutter, IsarDB, Supabase
- **Technical Challenges:** Integrate IsarDb and supabase in the same project
- **Solutions to Technical Challenges:** A mix of clean architecture and my own way of doing things

## **Lessons Learned**

- **What Went Well:** Achieve the goal of the project
- **What Could Have Gone Better:** The understanding of the architecture of the project
- **Improvements for Future Projects:** Learn more about clean code

## **Recommendations for Future Projects**

- **Recommended Changes in Tools and Technologies:** Adapting more of the clean code for future projects.

## Experience building the project

For this project I want to be able to swap between two databases, but without breaking the app, back then when I was taking a course about Flutter by Fernando Herrera, one of the things that I most remember of the course was the architecture or rather the folder structure of the project.

Having a `domain` , `infrastructure` or `presentation` folder was something new for me, learning about the `datasources`, `mappers` and `repositories` feels really weird at the beginning at least for me.

And to be complete honest, right now it’s one things that I still doesn’t understand at all, so I think that making a project about it will help me understand more about it. The stack for this project was flutter, the reason to choose flutter was:

1. I can look at the code of Fernando and see an example of the implementation
2. I want to make a mobile app.

### The project to be build

This app is really easy, it’s just a simple todo app, but with the difference that the tasks are stored in two differents databases one is `supabase` and the other is `isar db` a solution for flutter.

### Implementation of The app logic

The main part for the app to work are the datasources and the repositories.

### Datasources

How I understand what a datasources are, is the next concept:

> The origin of our data, maybe from an API, database, etc.
> 

In the flutter project in the `domain/datasource` folder I create a file to show  what the structure of my task datasource was. Something like this:

```dart
// domain/datasource/task_datasource.dart
abstract class TaskDataSource {
  Future<List<Task>> getTasks();

  Future<void> createTask({
    String name,
    bool completed,
  });

  Future<void> updateTask({
    required Task? oldTask,
    required Task? newTask,
  });

  Future<void> deleteTask(Task task);
}
```

Meanwhile in my folder `infrastructure/datasource` I define a file `task_datasource_impl` to be the one that extends the `task_datasource` after creating this file I added two files, one for isar db and other for supabase.

One of the things that I notice meanwhile writing this post was that I use the `task_datasource_impl` class instead of the `task_datasource` for my `isar` and `supabase` task_datasource.

For example let’s look at the `task_isar_datasource.dart` 

```dart
class TaskIsarDataSource extends TaskDataSourceImpl {
  @override
  Future<void> createTask({String name = '', bool completed = false}) async {
    final isar = await IsarConfig.init();
    final task = TaskIsar()
      ..name = name
      ..completed = completed;

    await isar.writeTxn(() async {
      await isar.taskIsars.put(task);
    });
  }

  @override
  Future<void> deleteTask(Task task) async {
    final isar = await IsarConfig.init();
    final taskToDelete = TaskMapper.entityToTaskIsar(task);

    await isar.writeTxn(() async {
      await isar.taskIsars.delete(taskToDelete.id);
    });
  }

  @override
  Future<List<Task>> getTasks() async {
    final isar = await IsarConfig.init();
    final tasks = await isar.taskIsars.where().findAll();

    final listOfTask =
        tasks.map((task) => TaskMapper.taskIsarToEntity(task)).toList();

    return Future(() => listOfTask);
  }

  @override
  Future<void> updateTask({Task? oldTask, Task? newTask}) async {
    if (oldTask == null || newTask == null) return Future(() {});

    final isar = await IsarConfig.init();

    await isar.writeTxn(() async {
      final taskToUpdate = TaskMapper.entityToTaskIsar(newTask);
      await isar.taskIsars.put(taskToUpdate);
    });
  }
}
```

So of course we are implementing the structure that our task datasource has, but the problem it’s that we are getting that from a impl file instead of the file inside the domain. So is one thing that I intend to improve in next projects.

> For those wondering how the supabase integration looks, here is a look at the file:
> 

```dart
class TaskSupabaseDataSource extends TaskDataSourceImpl {
  @override
  Future<void> createTask({String name = '', bool completed = false}) async {
    final task = Task(name: name, completed: completed, id: '0');
    await Supabase.instance.client.from('task').insert(task.toJson());
    print('supabase completed');
  }

  @override
  Future<void> deleteTask(Task task) async {
    await Supabase.instance.client.from('task').delete().eq('id', task.id);
  }

  @override
  Future<List<Task>> getTasks() async {
    final data = await Supabase.instance.client.from('task').select();
    final tasks = data.map((e) => TaskMapper.fromJson(e)).toList();
    return tasks;
  }

  @override
  Future<void> updateTask({Task? oldTask, Task? newTask}) {
    // TODO: implement updateTask
    throw UnimplementedError();
  }
}
```

### Repositories

What I understand about repositories is the next:

> They are a design pattern, basically we use them to abstract the access to the data of an application. This repositories has methods for getting information, inserting, deleting record for our data.
> 

In the app I have a `TaskRepository` class for defining the actions that can be done within a task here is the code for the class:

```dart
abstract class TaskRepository {
  Future<void> getTasks();

  Future<void> createTask({String name = '', bool completed = false});

  Future<void> updateTask({
    required Task? oldTask,
    required Task? newTask,
  });

  Future<void> deleteTask(Task task);
}
```

And in the `task_repository_impl.dart` I have the next code

```dart
class TaskRepositoryImpl extends TaskRepository {
  final TaskDataSource dataSource;

  TaskRepositoryImpl({TaskDataSource? dataSource})
      : dataSource = dataSource ?? TaskDataSourceImpl();

  @override
  Future<void> createTask({String name = '', bool completed = false}) {
    return dataSource.createTask(name: name, completed: completed);
  }

  @override
  Future<void> deleteTask(Task task) {
    return dataSource.deleteTask(task);
  }

  @override
  Future<void> getTasks() {
    return dataSource.getTasks();
  }

  @override
  Future<void> updateTask({
    required Task? oldTask,
    required Task? newTask,
  }) {
    return dataSource.updateTask(oldTask: oldTask, newTask: newTask);
  }
}
```

In the repository impl I use the methods inside the datasource passed to get the data and then return them to the user.

### How I use the repository on the UI

For using the repository on the UI (app) I use the statemanagment flutter bloc for handling which of the two datasources (isar or supabase) are active. This is the code for my `TaskCubit`

```dart

class TaskState {
  final TaskDataSourceImpl dataSource;
  final List<Task> taskList;

  const TaskState({required this.dataSource, this.taskList = const []});

  factory TaskState.empty() => TaskState(dataSource: TaskIsarDataSource());

  TaskState copyWith({
    TaskDataSourceImpl? dataSource,
    List<Task>? taskList,
  }) =>
      TaskState(
          dataSource: dataSource ?? this.dataSource,
          taskList: taskList ?? this.taskList);
}

class TaskCubit extends Cubit<TaskState> {
  TaskCubit() : super(TaskState.empty());

  void getTasks() async {
    final tasks = await state.dataSource.getTasks();
    emit(state.copyWith(taskList: tasks));
  }

  void createTask(Task task) async {
    await state.dataSource
        .createTask(name: task.name, completed: task.completed);
    getTasks();
  }

  void deleteTask(Task task) async {
    await state.dataSource.deleteTask(task);
    getTasks();
  }

  void updateDataSource(TaskDataSourceImpl dataSource) {
    emit(state.copyWith(dataSource: dataSource));
  }
}
```

Also I use a `data cubit` for handle the switch between the task datasource, here is the code for my cubit:

```dart
class DataState {
  final TaskDataSourceImpl dataSource;
  final bool onlineDB;

  const DataState({
    required this.dataSource,
    required this.onlineDB,
  });

  factory DataState.empty() =>
      DataState(dataSource: TaskIsarDataSource(), onlineDB: false);

  DataState copyWith({TaskDataSourceImpl? dataSource, bool? onlineDB}) =>
      DataState(
          dataSource: dataSource ?? this.dataSource,
          onlineDB: onlineDB ?? this.onlineDB);
}

class DataCubit extends Cubit<DataState> {
  DataCubit() : super(DataState.empty());

  void toggleState() async {
    if (!state.onlineDB) {
      emit(
          state.copyWith(dataSource: TaskSupabaseDataSource(), onlineDB: true));

      return;
    }
    emit(state.copyWith(dataSource: TaskIsarDataSource(), onlineDB: false));
  }
}
```

And finally in my UI I have two buttons one for isar db and another for supabase.

> You should switch the datasources according if the user has internet connection or not, but in this case, I wanted to keep it simple.
> 

```dart
// Some custom widgets that i use, the important part is the onTap method
CitCardItem(
  current: dataCubit.state.onlineDB,
  size: size,
  icon: Icons.wifi_rounded,
  name: 'Supabase connection',
  onTap: () => dataCubit.toggleState(),
),
CitCardItem(
  current: !dataCubit.state.onlineDB,
  size: size,
  icon: Icons.wifi_off_rounded,
  name: 'Isar DB',
  onTap: () => dataCubit.toggleState(),
),
```

### Final thoughts

I think this one a really good first approach to somethings like this, in the past I have never done somethings similar, so it was not just a interesting experience, but also this allow me to have guide to see what to learn, in future project I will be implementing more about this architecture and learning more about it.