DEV Community

Carlos Estrada
Carlos Estrada

Posted on

Integrating multiple databases in a todos app (Personal project)

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:

// 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);
}
Enter fullscreen mode Exit fullscreen mode

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

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);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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);
}
Enter fullscreen mode Exit fullscreen mode

And in the task_repository_impl.dart I have the next code

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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


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));
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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));
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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(),
),
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)