Flutter is a Google framework used to build natively compiled applications for mobile, web, desktop and embedded devices with a single codebase. You can easily start a new Flutter project by following the guide: Getting started – Flutter.
When discussing architecture on Flutter apps, there are many ways to structure your code and widgets. The main goal of this proposal is to make your project easy to scale and keep everything organized at same time.
Now our basic structure is set up! Don’t worry about all those folders and files, throughout the next topics we’ll cover the purpose of each one and why we need all of them.
Components: Atomic Widgets
Like React/React Native projects here at Cheesecake Labs, we work with Atomic System/Components to make our widgets more reusable and organized.
source: Brad Frost (2016)
In essence, we structure the components as:
Atoms: The fundamental components like custom texts, buttons, icons and typography.
Molecules: Basically, they are groups of atoms. One example is a list tile.
Organisms: They are an association of molecules, like a custom list or a form.
Templates: They are the final structure composed by organisms. Our pages/screens will use a template instance to set the real content.
Considering the information above, we can organize the components folder like:
Constants are the code that is immutable in your application. The following can be considered a constant:
Colors
Styles
Specific numbers
For example, if you need to change the color of the application or the size of the font that you are using, you’ll need to replace the old constant value in one file only.
The application core
The core layer is responsible for the services which the app will use all along its lifecycle. There are two core services we usually set up:
Dependency Injection
We can create a dart file and register our instances as the following:
It will allow us to provide dependencies to our classes, keep the instances easy to change for different implementations, and help separate the module layers.
Navigation
To help us work with navigation more efficiently, let’s create a custom navigator:
And to start using this navigator, you need to pass the navigatorKey to your
MaterialApp.
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
navigatorKey: RouteNavigator.navigatorKey,
onGenerateRoute: (routeSettings) {
return MaterialPageRoute(builder: routes[routeSettings.name]!);
},
...
);
}
}
By using this class, we’ll be able to navigate through the app with no context required, just by calling:
RouteNavigator.pushNamed(...).
Interfaces
When we develop an app, we usually need to communicate with an API or some external resource, such as a local database and sensors. So, the interfaces folder is the place where you store the files that will help you with this communication.
A common example is the creation of a Dio instance to work with HTTP Requests:
class HttpHelper {
final String _url;
late final BaseOptions _options;
late final Dio _dio;
Dio get dio => _dio;
HttpHelper(this._url) {
_buildBaseOptions();
_buildHttp();
}
void _buildBaseOptions() {
_options = BaseOptions(
baseUrl: _url,
responseType: ResponseType.json,
);
}
void _buildHttp() {
_dio = Dio(_options);
}
HttpHelper addInterceptor(Interceptor interceptor) {
_dio.interceptors.add(interceptor);
return this;
}
}
Middlewares
Middlewares are responsible for providing an extension to an event or action from your application. This can be an HTTP request interceptor, a logger or something similar.
Here you can see an example of a middleware that throws a custom error when the request to an API returns the status code 401:
class AuthInterceptor extends Interceptor {
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
if (err.response != null) {
if (err.response?.statusCode == 401) {
throw UnauthorizedException();
}
}
handler.next(err);
}
}
Modules: A hexagonal architecture approach
This folder is the most important, as it contains all modules that implement our features and its business rules. Here we’ll use an hexagonal architecture approach in a way that allows us to separate our application layer from the domain layer and the infrastructure layer. So, every module will be structured according to the following architecture:
With this architecture, the code will be more organized, but we also need to modularize it to make it easy to add a new feature, modify the old ones and make the code more reusable.
Separating the modules this way allows us to use different implementations for every layer. We can, for example, create multiple different screens with the same domain logic or even change all the data source providers without affecting the entire application. For example, our app is separated into independent pieces which are able to work individually.
Moreover, this approach supports testability, which is a good point for building a reliable project and improving code quality.
Conclusion
When you are dealing with complex projects that contain many features, this architecture is a lot of help when it comes to keeping everything organized, easy to modify, and extremely simple to maintain and to test the code along the development process.
This is just an efficient way we’ve come up with to structure our Flutter projects here at Cheesecake Labs and we hope it helps you as well.
Would you like to know more about how to use Flutter to get your project off the drawing board? Contact us!!