Despite the fact that Flutter already has a bunch of great features, it keeps evolving. For instance, the updated Flutter 3.0, released spring 2022, now supports development for 6 platforms including Windows, macOS, and Linux. Isn’t that a reason to choose Flutter?
There are currently over 500 000 apps built with Flutter, and it seems like this number will grow dramatically in the next year. It’s also great for implementing accessibility tools like text-to-speech, color inversion, etc. and for web development. We talked more about the benefits of Flutter for the web in this article and even proved that Flutter is good for PWAs as well as for mobile and desktop apps.
We at Surf have been developing Flutter applications since 2019. And we’ve dealt with projects of various sizes, from small apps to complex ones. From our experience we can say that Flutter’s suitable for projects of different complexity and business areas: from entertainment and e-commerce to foodtech and fintech.
Find out Flutter’s advantages for different business areas
Learn moreHowever, anything can be improved, and Flutter is no exception. That’s why Surf came up with Elementary, a Flutter library which helps create a clear architecture for complicated projects.
Read this article up to find out what exactly Elementary is and why it is a real time- and money-saving Flutter library.
Diving deeper into Flutter architecture
Why architecture matters
Defining application architecture is the thing that should be done before all. This is the most crucial stage as it affects the whole development process as well as the final result.
The right architectural pattern and properly written architecture itself altogether ensure that changes made in one code section won’t affect the rest of the system. Besides, they make sure the code is easily understood by all team members. As a result, the amount of time new team members spend on diving into the current code is reduced and it’s easier to control development for project managers and team-leaders.
To be reliable and efficient, a good mobile application architecture should meet the following requirements:
- SOLID principles: Single Responsibility Principle, Open-closed Principle, Liskov Substitution Principle, Interface Segregation Principle, Dependency Inversion Principle.
- KISS principle (“Keep it simple”), which implies that the code must be kept simple to lower the number of errors.
- DRY principle (“Don’t repeat yourself”), according to which developers should reduce repetition in software patterns to avoid redundancy.
- The Clean Architecture. It means that each application layer must be independent from the others and responsible for its own operations.
Architecture-related flaws and their consequences
Alas, not all developers share the point of view mentioned above and some even neglect all existing architecture patterns. But we must admit, if there weren’t such people, we wouldn’t clearly see what such a position may lead to and couldn’t identify the most common mistakes. The latter include:
- Mixing up the business logic and visual components. It entails increased code lines and, subsequently, breaks the SOLID principle. Such a project is extremely hard to maintain and test.
- Lack of separation into the main layers. The final code with messed up layers makes the application unstable, since changing one component is likely to affect the whole system.
We at Surf consider responsibility layer separation to be the main criteria for successful Flutter application architecture. There are several popular patterns like BLoC, Redux, MobX, and others. However, we believe that the best one that suits Flutter specifics is MVVM. That’s why we chose that exact concept as a basis for Elementary, our Flutter library that helps avoid the flaws we mentioned before and allows developers to write code that meets all the “clean code” principles.
Learn more reasons why our clients and users appreciate Flutter apps
Learn moreWhat is Elementary
Elementary is a cross-platform library and simultaneously an architecture package for Flutter, based on the MVVM model. Its main goal is to create a clear Flutter application code structure. It’s done by separating the responsibilities of the business logic, presentation logic, and UI layers. The package helps create the code in Flutter on the corresponding Model, View, and ViewModel layers. Elementary fits nicely into the overall logic and layered architecture of Flutter. We wrote more about its architectural features in this article.
While creating Elementary, we faced to some extent a challenging choice of what architectural pattern to choose. We were hesitating between BLoC, Redux, MobX, Vanilla and, at last, MVVM. We dwelled on the MVVM concept since it perfectly matches the Flutter’s architectural specifics and allowed our developers from Android and a new-born Flutter team to easily switch between tasks. We later on adapted the MVVM pattern to Flutter specifics by replacing “View” by “Widget” and got an MWWM concept. Let’s look at what’s under its hood and how exactly it works.
The Elementary library under the hood
To make it clearer and easier to understand, let’s examine Elementary’s internal logic on the example of one of Surf’s recent Flutter projects, the first neobank application in Pakistan.
It was an elaborate fintech project with numerous SDK integrations, a backend made with due regard for details, and a thoroughly designed interface. The development was challenging to a certain extent, considering the complexity of the project itself and the time frame set by the client. You can find a detailed description of particular solutions we used, app features, and other details of the development process in the case study. However, none of these would’ve been possible, and, apparently, it would have taken far more time instead of less than one year if Surf hadn’t had Elementary.
But first, look at what Elementary library architecture basically is:
As it is based on the MVVM pattern, Elementary library has three main layers. Let’s delve into details and talk about each of them.
ElementaryModel
This Elementary layer corresponds to the Model layer in MVVM. It’s responsible for the data storage & logic of data processing in the app. Within the ElementaryModel, the requests are sent to the network and other external systems, such as device memory and device sensors. For example, we connect a repository from which we can retrieve data and store them there.
In the model, we declare a variable of ValueNotifier class, which allows us to inform subscribers that the data has changed. It will send status information to the screen.
final ValueNotifier<VerificationStatus> verificationStatus;
It can take one of these values: confirmed, checking, failed or nonWhitelisted. And then let’s get the actual data from the external repository:
final status = await _verificationRepository.getVerificationStatus();
if (status != null) {
verificationStatus.value = status;
}
WidgetModel layer
It corresponds to the ViewModel layer in the MVVM concept. WidgetModel is a bridge between the ElementaryModel and the ElementaryWidget components. It reacts mainly to the events that happen on the screen. For example, let’s prepare the data received from the server for display on the screen by creating a getter:
ValueListenable<VerificationStatus> get verificationStatus =>
model.verificationStatus;
When we do this, some method is triggered on the screen, and the implementation of this method is written in the WidgetModel.
There are special fields responsible for the state, i.e. they contain variables that are displayed on the screen — and the WidgetModel can control them.
ElementaryWidget
This is the View layer in MVVM. It receives the data from the WidgetModel component and displays the changes that are made or being made. It should be as simple as possible, so it could quickly take data as input and subscribe to states that are described in WidgetModel.
In the ElementaryWidget, which is basically the screen of our application, we define the data that will be displayed on the screen depending on what status we get from the server:
final _titleMap = {
VerificationStatus.failed: VerificationStatusScreenI18n.failedTitle,
VerificationStatus.checking: VerificationStatusScreenI18n.checkingTitle,
VerificationStatus.confirmed: VerificationStatusScreenI18n.confirmedTitle,
VerificationStatus.nonWhitelisted:
VerificationStatusScreenI18n.nonWhitelistedTitle,
};
final _subtitleMap = {
VerificationStatus.failed: VerificationStatusScreenI18n.failedSubtitle,
VerificationStatus.checking: VerificationStatusScreenI18n.checkingSubtitle,
VerificationStatus.confirmed: VerificationStatusScreenI18n.confirmedSubtitle,
VerificationStatus.nonWhitelisted:
VerificationStatusScreenI18n.nonWhitelistedSubtitle,
};
ElementaryWidget “listens” to the verificationStatus variable and changes the display depending on whether the data in the variable defined in the WidgetModel has changed. We access verificationStatus via the ModelWidget component.
Two “Text” widgets depend on the data received from the server in this example, and each time they will change their text depending on the data received.
We end up with screens like this, depending on the data we get:
Element
This is kind of the key that makes the whole system work. Element basically has 3 functions:
- storing the WidgetModel
- making it work
- passing it to the Widget as a contract so it can present the display
Elementary has some additional tools to make the development process using the library even faster. These include, for instance, StateNotifier, which provides a quicker response to any changes in the properties of a state object. The Elementary library also contains tools like EntityStateNotifier, StateNotifierBuilder and others, which you can get familiar with in Surf’s pub.dev repository.
Why choose Elementary instead of other libraries?
Elementary vs Stacked
The main drawback of the Stacked library is that the widget has access to the code context, which makes it difficult to test the code. In Elementary, on the contrary, all context-related work is moved to the WidgetModel component.
Elementary vs States Rebuilder
States Rebuilder contains states injection tools, which provide an additional class that describes all dependencies. Dependencies become globally available across the project, and responsibility areas grow indistinct. As a result, the structure is not as clean as it could be.
Wrapping up
A well-designed architecture is highly important, since it’s directly related to the speed and the overall cost of development and stability of an app. To be considered high-quality, architecture must abide by the SOLID, DRY, KISS, or at least Clean architecture principles.
Elementary, in its turn, is a good choice for Flutter applications, because it perfectly matches the requirements of a well-designed architecture. Since there’s nothing superfluous in it, it wins the battle with other similar libraries like Stacked or States Rebuilder. Although the Elementary library is quite easy to use, developers should pay attention to details like treating dependencies in a certain way for better performance and specifics of each MWWM pattern layer.
If you share our vision on what a perfect Flutter application architecture should be, you’ll definitely appreciate Elementary in practice.
By the way, Elementary is an open-source package, so it’s available for review and testing anytime. We at Surf believe that public contribution is crucial for progress in any field. Besides, the open source nature of the technology used ensures that the customer doesn’t get a pig in a poke.