PS (Pre-article statement): This article is based on what I understand from clean architecture and my observations on its application in the mobile app development world. So, if you see any issue in my way of using clean architecture, please kindly inform me 🙂
First of all, if you don’t know what clean architecture is and why it’s a good thing to implement, please first check Uncle Bob’s blog post.
If you don’t still get what it is, there are many articles out there online and you can go through a couple of them. However, in short, the goal of using clean architecture is to have a codebase:
- That does not depend on some framework or library, and
- In which business rules (use cases and entities — these are the inner layers of our architecture) do not depend on user-interfaces, databases, web servers, or some external devices (and these are the outer layers).
In other words, dependencies can exist only from outer to inner layers. We put more general stuff in inner layers and are more likely to change stuff in outer layers. As a result, we can easily replace whatever in outer layers easily when we need and without affecting our core logic. And this core logic always stays testable as it’s independent of external factors.
OK, that’s enough for the explanation part! I know these words don’t mean much without showing a real-world example. Therefore, it’s time to get into how I use clean architecture in a Flutter app.
First of all, our app’s overall architecture is as follows:
The main/root project has three modules (Flutter packages) in it: presentation, data, and domain. Presentation and data modules are the outer layers of clean architecture, whereas the domain module corresponds to inner layers. That’s why the first two depend on the third one. In the overall picture, our root project depends only on these three packages, nothing else.
In terms of code, the main project has just
main.dart file under
lib folder. Here, basically what we do is to initialize these three modules and run the app. By initializing, I mean we perform whatever necessary for a module before starting the app; and there might be nothing necessary for some. So, our
main function looks something like the following:
Our first module to talk about is the presentation module. This module directly depends on the domain module in our architecture. It’s not allowed to communicate with the data module. Its package structure is as follows:
Almost all our code resides in
src package. So,
presentation.dart, the file which contains
Presentation class, decides what to “export”, meaning that it allows what will be visible to the root project.
src folder, we have the following packages:
featurepackage contains the packages related to all presentational features of the app. As an example, let’s say we have
profilefeature in our app. So, this package would contain at least the following:
ProfilePresenter, and a
widgetpackage which contains the related widgets used in
ProfilePage. I will explain what these classes are responsible for while I’m going through an example. So, please keep reading 🙂
widgetpackage contains the reusable widgets that are not specific to one feature but used by multiple features.
corepackage contains the “brain” of our app, so to speak. It includes all the classes regarding localization, styling, resource/asset handling, page navigation management, constants, etc.
entitypackage contains all simple data classes only related to the presentation layer.
utilpackage contains all utility classes only related to the presentation layer.
Another module in our architecture is the data module. Together with the presentation module, this module also exists in the outer levels of clean architecture. However, it has a dependency only on the domain module, and cannot talk to the presentation module directly.
Again, almost all our code resides in
src package where we have the following packages:
repositorypackage contains the implementations of the repository contracts which we will define and talk about in the domain module. More specifically, these implementations involve, but not limited to communications with web servers, third-party SDKs, or external devices.
helperpackage contains the helper classes that are commonly needed by the repository classes, such as HTTP client, local database handler, etc.
utilpackage contains all utility classes only related to the data layer.
The inner layer of our architecture is the domain module. So, the other two modules, presentation, and data, directly depend on the domain. This module is where we encapsulate all our business rules.
As you can already figure out from the other modules, the implementation resides in
src package; and
domain.dart, the file which contains
Domainclass, lists all the files to be exported and therefore used by the presentation and data modules.
src folder, we have the following packages:
usecasepackage contains all the use case implementations of our application. These are also called “application-specific business rules”. By just checking this package, you can understand what the app is doing. A use case class has just one method called something like
run, and it performs only what class name says based on the single-responsibility principle. For instance, we may have use cases such as
SetUserName, and we can utilize these for our
profilefeature that we mentioned before.
entitypackage contains all the data classes that are used by the entire application. We can encounter the usage of these entities in any module.
repositorypackage contains the repository contracts. These are the interfaces that our app expects from the data module to implement.
utilpackage contains all utility classes only related to the domain layer.
Flow of data
Last but not least, in order to showcase how data (actions/reactions) flows through these modules, I want to provide an example for a particular case. This will be about user’s interaction with
ProfilePage to edit his name.
So, the flow is as follows:
- User clicks on profile button and
ProfilePageshows up. This action also triggers the initialization of
setState()method. Each controller also holds a presenter instance which is
ProfilePresenterhere in this case. So, after the initialization, the controller starts observing the observable
userinstance in the presenter.
- User edits his name on profile page.
- Profile page widget triggers
onEditName(newName)method in the controller.
- Controller triggers
setUserName(newName)method in the presenter.
- Presenter invokes
SetUserNameuse case and waits until it gets success or error.
- Use case communicates with a
setUserName(newName). It doesn’t know about the implementation, only cares about the interface itself. The implementation resides in the data module, and here we save the new user name to the cloud and return the updated user object.
- Use case notifies the presenter with a success message which contains the updated user object.
- Presenter updates its observable user instance with the new object.
- This triggers the observer in the controller.
- When the observer in the controller gets the trigger, it uses
setState()method to rebuild the user-interface with the new user data, specifically the user name.
That’s all for this article. I hope this inspires at least some of you. As I said at the beginning, if you see any issue or have a question/comment, let me know so that we can discuss it.
Stay safe and code cleanly 🙂