Mobile app clean architecture in practice
Introduction
App architecture from the start dictates how easy or hard it will be to work on your new project. In this blog, we shall concentrate on architectures applicable to mobile app projects.
The main criteria for choosing an appropriate architecture for your project is for an app to be easy to implement a feature, upgrade an existing feature, or just maintain it. Also, it should be testable and less prone to errors. Each component in the architecture should depend upon abstraction, not on a concrete class. Some of the most used architectures are MVVM (Model-View-ViewModel) used on Android projects, MVC (Model View Controller) used on iOS projects and finally more and more popular Clean Architecture. All architectures share the same goal: Separate concerns and logic into layers. Let’s see what it actually is.
App architecture overview
“Clean Architecture: A Craftsman’s Guide to Software Structure and Design” is a book written by Robert C. Martin, aka Uncle Bob. Its main motive is to define and bring discipline into the app architecture, to achieve separation of concerns as much as possible. In the book, Uncle Bob defined an onion-like layered architecture where in the center are entities, the crucial classes for any app. It shares that with the Hexagonal architecture. Actually, Clean architecture is a more refined version of the Hexagonal architecture, which Uncle Bob refers to in the book. Their common goal is to decouple business logic from data sources. Let’s look at the architecture.
There are three layers in the app: data layer, domain layer and presentation layer. In short, the domain layer is the core of the app and it has no dependencies, this layer holds entities and use cases/interactors. The data layer depends upon the domain layer and its main goal is to fetch or store data on remote or local data sources. Finally, there is the presentation layer which also depends upon the domain layer and its main goal is to show data and interact with a user. You can notice in the picture that there are two databases. I shall use them to demonstrate the goal of Clean architecture, and how easy it should be to change data sources with no impact on the rest of the application.
!IMPORTANT! I shall take some time and take you through this process step by step, so if you find this blog too comprehensive, feel free to skip parts, use subtitles to navigate to what you are interested in.
Domain layer
Firstly, we should start from the core of the application, the domain layer. This layer consists of:
- Entities (main data holders)
- Repository interface definition (communication with datasources)
- Use cases (core business logic of app)
Maybe you are wondering how can this layer be completely independent. It fetches and saves the data on various data sources, it must depend upon the data layer, right? Well, we want for data layer to depend upon the domain layer, the domain layer must not have ANY dependencies. That is why a repository interface is defined on the domain layer and its implementation is written on the data layer. That is called a Dependency Inversion Principle. Here is the layer structure:
domain
├── entity
│ └── project.dart
├── repository
│ └── project_repository.dart
└── usecase
├── create_new_project_use_case.dart
├── get_projects_use_case.dart
└── delete_project_use_case.dart
Entity
The main and the only role of entities is to hold the data in well structured classes, simple as that. Here is the code:
Use case
Use cases are wrappers for business logic. Everything related to the core logic must be written in a use case class. In this app we have a couple of use cases for project manipulation. It is important to emphasize that the following class separation is not the only correct way of doing it. You can have a single ProjectUseCase class and each use case can be a method. I decided to separate the use cases into classes to follow the original idea of the Clean architecture and the principle that a class should have only one concern.
Repository
As we said before, here we need interface definition for our repository and that is all we need, the rest will be done on the data layer.
Data layer
Our app has 2 available datasources, Isar database and Hive database. The main question we should ask ourselves is how we should decouple repository from datasource. In case where we could have multiple datasources, the repository should talk with them over the interface, it should depend upon abstraction. In this project, we shall create DatabaseManager interface. Every new database should implement DatabaseManager to enable app to communicate with database. However, as you can see on the diagram, each database must have its own model. Let’s go with the Hive implementation first and take a look at the layer structure at the very beginning.
data
├── converter
├── database
└── repository
Our data layer consists of converter, database and repository packages. Since data layer depends upon domain layer, we do repository implementation here as said above. Repository communicates with the database, but for each database we need a separate model, therefore the converter package. Let’s take a closer look into the database package.
Database
database
├── hive
│ ├── model
│ │ ├── project_hive_model.dart
│ │ └── todo_item_hive_model.dart
│ ├── hive_datasource.dart
│ └── hive_manager_impl.dart
├── isar
│ ├── model
│ │ ├── project_isar_model.dart
│ │ └── todo_item_isar_model.dart
│ ├── isar_datasource.dart
│ └── isar_manager_impl.dart
├── model
│ ├── project_db_dto.dart
│ └── todo_db_dto.dart
└── database_manager.dart
We created a package for each database, a model package and a DatabaseManager class in the root of the database package. Each database has its own model, datasource, which initializes the database, and implementation of the DatabaseManager interface. An example of Hive model, datasource and interface implementation:
While reading this, you can see that two converters are injected into the HiveManagerImpl. We need them to convert datasource specific model class to more general DTO class which repository receives. To see what we are talking about, here is the DatabaseManager interface:
Okay, I understand that this might be a complicated topic to discuss, but stay with me, we shall figure out all of this. The problem is that DatabaseManager must be as general as possible in order to enable decoupling in our code. To achieve that, we need ProjectDbDto class which contains necessary data, but it is more general, it is not tightly coupled to Hive or Isar models. Here it is:
Now we have everything ready in the database package to talk with the repository, to fetch or save data while doing it in a clean, loosely coupled way. Now we need converters and repository implementation.
Converter
converter
├── converter.dart
└── project_converter.dart
This package is quite simple, it contains only converter interface and implementation of it for the specific conversion use case.
Here are 4 implementations of Converter interface. Two of them are for ProjectDbDto and HiveProjectModel while the other two are for ProjectDbDto and Project, where Project is the entity class from domain layer. To make it more clear, here is the diagram to explain all.
Repository
The final step in data layer is to create the implementation of repository interface.
With all this done, we achieved what we initially wanted and that is to decouple communication with datasource via interface, via abstraction. Now we are able to add new datasource, for example SQLite or MongoDB database. All we need is to create implementation of DatabaseManager called SQLiteManagerImpl or MongoDBManagerImpl and inject it in ProjectRepositoryImpl class. No other changes are required. This is what should we all strive for when creating a software, isolate the business logic and create maintainable, readable codebase.
Presentation
The main point of blog is done in the data layer. To complete the whole app, we need a presentation layer. Its structure looks like this:
presentation
├── riverpod
│ └── project_provider.dart
├── screen
│ └── projects_screen.dart
└── widget
├── project_card.dart
└── project_picker.dart
Riverpod
For state management, Riverpod is chosen because it is versatile and easy to use. Here we inject use cases from the domain layer.
Screen
Every mobile app needs screen to present data and interact with its user. Here is an example:
Widget
Everything in Flutter is a widget, but here we put widgets that are shared across the screens of project feature. Most often they are pretty large files because of styling, so if you are interested to see those used in here, please check out the repository. Link of the repository is at the end of this blog.
Dependency injection
One last piece of the puzzle, which connects all dots into a working application, is missing and that is the file where all dependency injection is done.
Now if we want to change the database, all we need to do is inject isarManagerProvider instead of hiveMangerProvider.
Hive database:
Isar database:
Conclusion
This is it. The goal is achieved, we created an app that is easy to extend with more features, easy to navigate and read the code and, of course, easy to maintain because our codebase is not tightly coupled, therefore it is flexible to work with. I hope you enjoyed this blog. I know there was a lot to cover, but we did it. I am really interested in your opinion about this approach and what you would change.
From junior to senior position, you will or you already have found yourself on the project with a bad codebase, which is very hard to maintain. If this is something new for you, I kindly recommend you to learn this architecture and the way of thinking behind it. That way we all contribute to the better projects on which we all shall work happily and discuss it with our colleagues. I would like to thank you for you time reading this and wish you a lot of happy coding! :D
If you liked this article, you can find the full repository here: https://github.com/comsysto/Todo-mobile-flutter-app/
Show your support by clicking and holding down the clap button until it reaches a number you’re happy with. If you’d like to talk more about your use-case, feel free to contact us.
This blogpost is published by Comsysto Reply GmbH