Architect your Flutter app the clean way with BLoC
--
To start with, we all know that working solo on a project will be an easy and smooth experience. However, this not the reality in the industry.
One of the essential skills for a Software Engineer is to be able to work on large projects and collaborate with other engineers.
And some of the main areas in software engineering that will make collaboration much easier are code maintainability and readability. Basically, if the code is written in a clean and systematical approach, then fortunately you can add new features on top of it without suffering 😁
For front-end apps, we need to handle three things mainly. Which are, what the user interacts with, the logic performed by the app, and fetching data used in the app.
The combination of the three specified categories is known as “Three-tier Architecture”, which is followed mostly in many of the front-end apps.
The main idea of this architectural style, is to implement separation of concerns. Which means each layer is responsible for one thing only.
For example, if you want to change the UI of the app, then you know that you’ll be dealing with the “presentation” layer.
Another example is if you want to change something in your app APIs, then directly you’ll go to the “data” layer. I hope that shows you the importance of a clean architecture 🧐.
I think that is enough for an introduction. However, before we start, I need to emphasize on one important thing.
Which is there no one optimal way to architect your app, this is only what I’ve found to be scalable and maintainable for most of the projects that I’ve worked on.
Layers Interactions 🔄
Before I explain each layer separately, we first need to fully understand the interactions between them.
BLoC is the cornerstone of this architectural style, it will basically take “Events” from the user in the UI, then perform some logic, and based on the result of the logic performed it will emit a new “State”. Afterwards, we are going to display UI based on the new “State” value.
The beauty of BLoC state management solutions, is that we can keep track of the BLoC’s state history, which will add a major advantage when trying to debug the app.
Also by applying BLoC, we can describe our system to be an Event-Driven one. And as a result, we can easily have a high level design of a feature, since it is just an interaction of “Events” and emitting new “States”.
Since now you have a brief on how BLoC state management works, it is time to describe the interactions between the layers.
In the beginning, the event will be fired from the UI by the user, for example the user presses the Login Button. That “Event” will be sent to the BLoC, and I want you to think of the BLoC as the “Center of the operations”.
The BLoC will receive the “Event”, and then call the usecase to perform whatever business logic needed. And here one point to mention, is that the usecase is simply an instance of a class, and it will be injected in the BLoC. Hence, we can access it directly from the BLoC itself.
After reaching this point, we can have two cases. If we don’t need to get data either remotely or locally, then the logic will be performed in the usecase and the value will be returned to the BLoC.
When the BLoC receives the value from the usecase it will emit a new “State”, and as a result the UI will be re-built based on the new “State”.
That was the case when we don’t need to fetch data. However, most of the functionalities requires data fetching. The layer responsible for this is the data layer.
Getting back to the flow of actions, the usecase will invoke the repository to fetch the data and return it in the form of an entity.
The repository will communicate the data sources to fetch the data.
If the data required is stored locally, then it will call only the local data source. Otherwise, it needs to communicate with the remote data source.
Keep in mind that sometimes we might use both local and remote data sources in the same repository for caching purposes.
Once data is fetched from the data sources, it will be sent back as DTO to the repository.
The repository will be responsible to convert the DTO into an entity and send it back to the usecase.
Afterwards, the usecase can apply all the logic needed in this step.
Next, we will use the returned value from the usecase inside the BLoC to determine which new “State” to emit.
I know that this might sounds difficult, but believe me you’ll understand it fully when I discuss each layer separately 😁.
So now after reading this thorough description of an event’s journey I want you to give a second glance to the first diagram, and I hope it will make more sense to you now.
Folders Structure 🗂
As your app scales and more features being added to it, the project’s folders structure might be a nightmare if not managed properly.
So let’s think of it this way, each file in the project is either used for a specific feature, being commonly used among the whole project.
For instance, when you are building an app from scratch, then mostly you’ll have common buttons to be used all over the place, textfields, and many other reusable widgets. These are an example of the shared files to be used in the project.
However, sometimes you design a widget specifically for one feature only. And you know that it will only be used there.
For instance, you want to have the user gender as a part of the authentication feature. In that case, you know that this widget will only be used in the authentication feature and nowhere else.
From the two examples above, you clearly noticed that sometimes we need to share widgets or files overall the app, and sometimes we don’t.
And based on this decision I always structure my files.
Note that utils classes, colors, theming, and many more are also considered to be common.
In addition, we will have a common “presentation” layer, where we gonna save the common widgets, dialogs, and pop-ups overall.
Also, a common “domain” layer where we will have common entities and usecases.
Lastly, a common “data” layer to fetch data commonly used across the app.
For example, read when is the last time the user opened the app.
And all those common files will be saved in a folder called “core”.
Sometimes you need to add more folders, for example validators, services, and list goes on. So add whatever you think will be common in your app.
The main idea is just to group the commonly used files in one folder called “core” .
The second main folder will be called “features”, and I think the name is self-explanatory.
Each major feature in your app will have a separate folder with its own “presentation”, “domain”, and “data” layers.
The beauty of this structure, is that you will not be lost when you want to modify existed code.
For example, you want to change something in the UI of the homepage. Then you’ll go directly to “features” folder, and since we are talking about homepage then you’ll go to “home” folder. And finally you’ll open “presentation” folder since what you want to change is the UI.
Imagine you are onboarded on a new project that uses the same folder structuring, I assume that you’ll have a smooth onboarding 😁
So far we have discussed the high-level architecture, layers interaction, and folders structure. Now it is the time to give a brief about each layer and their components.
Presentation Layer 🎨
From the name itself, presentation layer is meant to handle all what the user can see on the screen. Mostly in this layer, we are going to have the screen itself as a whole, and the widgets used inside it, and also the BLoC used for that screen.
As you can notice, we have three elements inside the presentation layers.
Let me talk about each one individually.
bloc: inside it, we will create the BLoC that will handle all the logic and state changes inside the home screen.
widgets: this folder will contain all the widgets used specifically for home screen, notice that we have a file called “body.dart”.
Basically this file will handle what will be shown inside the screen.
home_screen.dart: this file will be the placeholder and the scaffold for the home screen, and for its body we will use “body.dart”. In this file, also we will provide the AppBar if needed, and also provide the BLoC to the screen.
Domain Layer ⚙️
Wrapping all the app’s business logic inside the domain layer, will give us the freedom to easily change the state management solutions.
Because if we write all the logic inside the BLoC itself, then it will be hard to change to another state management solutions such as Riverpod.
For sure we still going to have a business logic written in the BLoC, but as you can notice. All the heavy logic will be performed in the usecase instead.
Now whenever we want to change the state management solution, we just need to change the small layer in between. With that stated, we won’t have to re-write the heavy part of the logic again.
If we don’t depend on usecases to handle the heavy part of the logic, then it will be challenging to change the state management solutions.
However, it depends on the case and the level of maintainability you seek in your codebase.
The other component we will have in the domain layer is the “entity”.
An entity is a capsulation of whatever data needed to represented in the UI.
For example, if one of the features in the app is to display ToDos. Then we will create an entity for that. Which will be much easier to deal with than a json object.
Also, it will be easier to share entities between widgets than sharing a json object.
In addition, we can embed some validation logic as a function inside the entity itself.
For example, having a function to check if the due date if a todo is passed or not.
Later in the data layer section, we will discuss the difference between DTO “Data transfer object” and entities.
Data Layer 💾
Now talking about the last layer, which is sometimes being referred as the “infrastructure layer”.
In this layer we have three main components, which are repositories, data sources and DTOs.
Let’s start from the lowest level, which is the data source.
Data source’s main job is to fetch data from a specific destination.
Some times we need to fetch it externally via http requests, in that case it will be a remote data source.
And in some other cases, you might have to fetch data locally from the local storage of the device. In this scenario, you will have to use a local data source.
As you can notice from the diagram, the data that will be returned to the repository will be in the form of a DTO.
We know that when we fetch data from the network, then mostly we will receive the response in the form of a json object, and dealing with json files directly might cause errors.
The issue happens usually when we try to access the data inside that json object by keys manually.
Imagine using the wrong key inside the json object by mistake. For example, instead of accessing the data this way ‘json[“name”]’ , you wrote ‘json[“namee”]’ instead by mistake.
DTO will solve this problem by parsing the data directly from the response “json” into an object.
And usually we use packages such as “json_serializable” to handle this parsing.
After creating a DTO, we don’t have to access the data by using keys. We will be dealing with solid objects and its variables.
We’ve discussed previously in the domain layer about entities.
So know you might wonder, what is the difference between and order’s entity and order’s DTO?
You are right, this might cause some confusion since both of them are objects that holds the order data.
But the main difference, is that we will use only the required data to be shown in the app inside the entity.
On the other hand, DTO will parse the response as is and store all the data returned from the data source.
For example, imagine that we want to display a list of orders. By showing only the order id, order price, and issued date.
Hence, it is clear that for our Order entity we will have only three variables. Which are the id, price and date.
But sometimes we don’t get the data the way we want it from the back-end.
Take this as an example.
It is clear that we will only need the first three values in our UI.
So here comes the difference, a DTO will be an object that contains all these ten values whether it is shown on the UI or not.
While the entity will only have the first three values which will be shown in the UI.
Now, we’ve reached to the last component in the data layer, which is the repository.
Repositories will be the wrapper of the data sources needed to achieve the business logic.
By applying this, the usecase will be dependent only on one file even if the data is being fetched from different data sources.
This diagram summarizes the whole story.
Basically, the usecase want to have a list of order entities to give it back to the BLoC so it can be displayed on the UI, and it does not care if the data is being fetched locally or remotely.
Now we can have this small logic inside the repository, first we will check for internet connection.
If we have it, then the repository will call the remote data source. Otherwise, the local data source will be called.
After receiving the data, it will be converted to whatever format the usecase wants “in this case List<OrderEntity>”, and then return it back to the usecase.
The good thing is that all this magic is happening behind the scenes in the repository.
Which means, the BLoC and the usecase does not have to know about how the data is being fetched explicitly.
Conclusion 🏁
Remember that your app’s architecture should help you on the long run, so you can add new features smoothly.
At the same time, if you find that having a simple architecture is helping you and your team to have a faster deliverables, then stick to that decision and don’t try to over-engineer your project just to look cool 😅.
Do whatever you think might help you to have a maintainable and readable codebase, because mostly that is the main point of having a clean architecture.
In addition, I know that what I’ve just presented in this article might not be the optimal and cleanest way possible. It is just to share the awareness and the knowledge of having a clean software architecture.
I really would appreciate sharing your thoughts on the comments section of this article, so we can all share knowledge an learn from each other 🧐
Feel free to reach out to me at my Linkedin account 😁