Folder structure for Flutter with clean architecture. How I do.
Organizing your folders and files is painful, especially on big projects that can easily suppress 1000 files to be managed.
Files should not contain hundreds of lines. For the sake of our mental health, we can not have high coupling and seek to implement the Single Responsibility Principle.
Generating high-quality code usually leads to small files, which leads to more files. Then our problem is created: How do we organize all these files efficiently?
Working with Clean Architecture
The concept of “Clean architecture” is broad and unclear, but when I say: “I’m using Clean Arch on the project”. I’m specifically talking about The Flutterando’s Architecture Proposal!
A brief look at the proposal
You will separate the code into 4 layers: Presenter, Domain, Infra, and External.
- Presenter — Our UI components, basically everything that it’s a widget or a widget’s controller.
- Domain — The app’s core. All entities and business logic will be held here.
- Infra — Support the Domain layer by adapting the data coming from the external layer through models, repositories, and services.
- External — Classes to wrap functionalities from third-party libraries, sensors, SO, storage, and any other external dependency of our app.
There is no bulletproof solution, but the clean arch works great in most situations for me, especially after adding some practices that I describe in the article Improve your clean architecture on Flutter apps.
For those without familiarity with this architecture proposal, it can be confusing to know the meaning of specific terms, such as “entity”, “controller”, etc.
We will see more detailed explanations throughout the article, but explaining the proposal is not the goal. Don’t hold yourself to read the official documentation.
Clean arch + Modular arch = Happy developer 😀
Modular architecture aims to gather related content in one place called a “module”.
Each module represents a major responsibility of the system and every module have restrictions to communicating with other.
We can see it as a system designed to increase decoupling and help different teams and experts work on the same source code.
To create a Modular Architecture on Flutter, we have the flutter_modular package based on the modular system of the Angular framework.
It splits our apps into modules, each with its pages and dependencies. Once the user exits a module, all the dependencies are disposed of.
This package also comes with tools for dependency injection and system navigation.
Together with the pubspec.yaml file, there are 3 directories that you will have to work: lib, assets, and test.
Nothing new here! You probably already use them, as the lib and test are auto-generated folders when you create a flutter project and the use of an asset folder is recommended by the flutter team.
Yet, I should define their use for newcomers. After, we will focus our attention just on the lib folder.
A place to keep all non-code files, such as images, fonts, icons, videos, etc. Anything that is used resource by the app can be here. Here is the official definition of what is an asset:
An asset is a file that is bundled and deployed with your app, and is accessible at runtime. Common types of assets include static data (for example, JSON files), configuration files, icons, and images (JPEG, WebP, GIF, animated WebP/GIF, PNG, BMP, and WBMP). — docs.flutter.dev
Where your dart files will be! Put the code here, just that. =)
All your test files will be here. Its structure mirrors the lib folder.
For instance, a test for the widget user_register_form.dart located in
Should be created as
This folder includes files that execute the main function for each Flavor.
common_main.dart file that executes the common commands of all Flavors.
For cases where Flavors are not used, we can switch this folder and its content for only a
Wait! JSON files? Shouldn’t they be in the assets folder? Well... Yes! This is my sin. The correct place to put JSON files is in the asset folder. But the localization package requires keeping it in the lib file.
Of course, I just do that because the package forces me. But if you change this dependency and use, for instance, the easy_localization package, you should remove the i18n folder from lib.
With that said, I still prefer the localization lib because it’s easier to use and have fewer open issues.
the easy_localization package manager already said that he is not actively maintaining the package anymore, creating a red flag about using it.
A folder to place all shared logic that cannot be represented by the Clean Architecture layers. For instance, regex strings, mixins e classes of utilities.
- Configs — Any initial configuration needed for the system. For example, the firebase initial configuration.
- Constants — App contacts strings, like routes names and regexes.
- Extensions — Dart Extension methods.
- Mixins — Dart Mixins.
- Utils — Utility classes, like CurrencyFormatter or DateUtils.
- Validator — classes that validate data, like fields, phone numbers, documents, etc.
Again, this is my personal experience, usually, these folders are enough to hold all classes that I need effectively. Depending on the project, I do create other folders to avoid the overuse of one of them (usually, the utils).
A folder containing all app modules. Inside each module, we will create the necessary clean code layer for each page of the app.
The main module and the root widget also are created within the modules folder.
Each layer has stored specific types of classes. They are:
Widgets — visual components classes.
Controllers — widget’s state management classes.
Entities — Classes to store data.
Usecases — business logic classes.
Models — Classes that extend from Entities to transform data.
Datasources — Classes that connect with external APIs (use the HTTP driver).
Services — Classes that do not connect with external APIs (do not use the HTTP driver).
Drivers — Classes to isolate external libs or system features.
Since the External layer has only one type of class, we will just create a driver's folder directly.
Let’s take an example to analyze the details of the structure of a module. I selected the auth module, which is every screen an unauthorized user can access, like login and user registration form.
Inside each module folder, we will have a module file with the same name that describe the navigation routes and dependencies used in the module.
I like to separate the module into pages. In this case, the user has access to 3 pages: Login, Register, and RecoverPassword.
Inside our login folder are the clean arch layers required for the login page.
As a module is divided by pages, we can encounter the login page widget and its controller at the root of the presenter folder.
The other widgets can be found in other folders like the“widgets” one. Feel free to organize your widgets at the occasional convenience.
Also, there is a “dialogs” folder. But could have a “views” folder if a PageView widget was needed to build the page.
Widget and Controller are the only types of classes that don’t have their own folder, they always should be together because of the high correlation.
It has an AuthEntity class that stores the username a password for the login functionality.
Any business logic is executed by the Usecase. But what are the possible businesses logics for a login feature?
Well, if there isn’t. You still need the domain just to invoke a Datasource. The absence of business logic is not an excuse to break our architectural rules.
Yet… Does your user keep authenticated after closing your app? If he/she does, then you need to store the auth token locally, this is business logic.
Do you have to send analytics data when the user logs in? This is also business logic.
Do you note the auth_datasource.dart and auth_datasource_impl.dart files?
Interfaces are created for Datasources, Services, and Drivers. The <name>_<layer>_impl.dart file is where the code is. Using interfaces increases decoupling and helps to build tests with Mock classes.
If the file’s type is a Datasource you already know it queries the API.
The domain gives an AuthEntity to my Datasource, which uses an AuthModel to convert its value for the API format.
Why we can call our API and have not a driver folder for the HTTP client? Because it is in the Shared module!
Besides our modules to hold pages, we have the Shared module.
We will put in this module any type of class that is shared between modules. The best example is the HTTP client driver.
This is also a module to share your theme widgets, like buttons, text fields, switches, etc.
Am I happy with my folder structure?
Hell no! Why should I?
That sinful i18n folder, someday I expect the localization package to fix it!
I never know if the driver that I seek is in the current module I working in or in the Shared module.
Have you seen the utils folder? What is the exact definition of a utility class? If I programmed it is because it has a utility for me. It’s just there because I can’t think of a proper name.
Theoretically, field validations are defined by our business logic matters, but we need to set them in the TextField widgets on the presenter layers. Maybe input validations should be in the Domain inside the Shared module. Yet, they are in the core folder because they are general utility functions.
What a headache! But still better than what the past me was doing. And I hope things keep getting better.
So, what do you think about helping me? I shared this knowledge and thoughts to support devs with the same problems and to search for some feedback.
Do you know any better or not-spoken solution about file structures for Flutter apps?