In my previous article, I had talked about designing and architecting Flutter applications using the Stacked framework. In this article, we will see how one can design a Flutter application using Clean Architecture.
If you haven’t read that article, I would highly recommend going and read that article first. I will take a lot of references from there (like Retrofit, DI, code generator using build_runner, etc.) in this article. Below is the link to that article.
In this article, I would be focusing on the implementation part of clean architecture rather than what is clean architecture. You can read and understand what is clean architecture from the below link:
Clean Coder Blog
Over the last several years we've seen a whole range of ideas regarding the architecture of systems. These include…
Why Clean Architecture?
An MVVM (or MVC/MVP/MVI) architecture is good enough for most of the applications. But when the application grows, it becomes hard to maintain the huge codebase, and sometimes, it requires (a substantial amount of) refactoring efforts to separate the responsibilities of each and every layer. And then clean architecture comes into the picture.
An important goal of clean architecture is to provide developers with a way to organize code in such a way that it encapsulates the business logic but keeps it separate from the delivery mechanism. The main rule of clean architecture is that code dependencies can only move from the outer levels inward. Code on the inner layers can have no knowledge of functions on the outer layers.
Let’s first see the visual representation of the clean architecture
Based on this, below is the implementation of the clean architecture solution in our Flutter application
And the project structure of our application
Understanding of modules
We have divided each and every layer into modules. Each of these modules (except the application module) is created as a Flutter package. The below diagram represents the relationship (or dependency) between the modules.
- flutter_clean_architecture: This is the entry point to our application (or application module). This module is dependent on the core and presentation modules and initializes these modules.
- core: Core is one of the main modules where you can find all the common implementations (like navigation, dialogs, toasts, etc.). Any other common utility classes should be defined here. This is an independent module.
- domain: Domain is the module where all the business rules are defined. It is a module that is independent of the development platform i.e. it is written purely in the Dart and does not contain any elements from the Flutter framework. All the use cases, entities, etc. are defined in this module. As the core module, this is also an independent module. One important thing to keep in mind is this module defines only the abstraction of the business rules. Implementation of this module is defined in the data module.
- data: Data module defines the implementation of the domain module. Interaction with database and network calls are defined in this module. Since this module provides the implementation of the domain module, it depends on the domain module.
- presentation: This is the module where we define all our feature modules (like login, article list, article details, etc.). This module uses the core module to use the common features (like navigation), domain module to use business rules (like use cases), and data module to resolve the implementation dependencies of the domain module. This module initializes the data and domain modules.
How implementation dependency of the domain module is resolved by the data module?
Here comes the magic part. Since presentation modules communicate with the domain module, how the actual network call made through the data module? The answer is through DI. We are using get_it plugin to define the use cases and repositories dependencies of domain modules which are resolved by the data module (as data provides the implementation of domain classes).
How does the data module work?
The responsibility of the data module is the get the data from different data sources like databases or networks. For the network call, we have used the retrofit plugin. For the database, I have used floor plugin. I choose floor (over sqflite or other database plugins) as it is very similar to the Room persistence library of Android (I am coming from the Android background :D).
How does navigation work?
As mentioned earlier, presentation is the module that defines all the features of the application. As it defines all the feature screens under router.dart file, I used the auto_route plugin to build and generate router for all the defined screens. The application uses this routing info from the presenter module to pass routing information to the core module. Why? Because core provides the navigation service to handle the navigation in the entire application. So, it needs to have the details of the application routes.
If you see the definition of CoreViewModel under the core module, you will find that I have created an instance of NavigationService which I am using in my presentation module. To use DialogService or other core module services, you can do it in the same way, or you can pass the instance of the service module to the constructor of your view model like below:
and use this service like this:
If you think about how I was able to get an instance of SnackbarService in ArticleDetailViewModel, just look at the @injectable annotation on top of that class :)
With this article, you might get an idea of how to implement the clean architecture in the Flutter application, how to make network calls, and get data from the database. In next article, I will talk about the data caching strategy for the offline-first Flutter application.