Mobile app clean architecture in practice

Filip Kisić
comsystoreply
Published in
9 min readSep 19, 2023

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.

Simplified diagram of app architecture which consists of three layers: data layer, domain layer and presentation layer.
Mobile app architecture diagram

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:

Code of class Project

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.

Code of class Create New Project use case
Code of class Delete project use case

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.

Code of interface Project repository

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:

Code of class Project Hive model
Code of class Hive datasource
Code of class Hive manager 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:

Code of interface Database manager

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:

Code of class Project Database DTO

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.

Code of interface Converter
Code of class Project converter
Another image of class Project converter

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.

Simplified diagram which explains relation between database models, DTOs and entities

Repository

The final step in data layer is to create the implementation of repository interface.

Code of class Project Repository implementation

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.

Code of class Project provider

Screen

Every mobile app needs screen to present data and interact with its user. Here is an example:

Code of class Project screen

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.

Code for dependecy injection

Now if we want to change the database, all we need to do is inject isarManagerProvider instead of hiveMangerProvider.

Hive database:

Dependency injection done with 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

--

--