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:
- I can look at the code of Fernando and see an example of the implementation
- 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);
}
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);
});
}
}
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();
}
}
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);
}
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);
}
}
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));
}
}
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));
}
}
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(),
),
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)