Elementary 

A new approach to architecture in Flutter apps introduced by our tech lead
Contents

Project idea

What is Elementary, and why was this library created?

Surf set about using Flutter in 2019. Since then, we have put together a sizable Flutter team, contributed to the framework, empowered and supported the community of experts through podcasts, byline articles and our own architectural standards. We constantly refine and polish our software development process. One of the solutions homegrown at Surf was the Elementary library.

Michael Zotyev, our Flutter team’s tech lead, came up with the idea. The prospects this library opened scored points with tech leads on other projects, who, as a result, were willing to try out the new solution and offer their feedback. For example, Vladislav Konoshenko shared his real-life experience with Elementary in a series of articles. Other developers followed suit, contributing to the library: Alex Bukin, for example, wrote the IDE tooling. Over time, the user base grew bigger. Today, our pool of contributors is no longer limited to our team; it now includes some external contributors as well.

We are currently using Elementary in most of our Flutter projects to provide them with clean architecture and testable code. It is especially useful in e-commerce and fintech projects with animations, drop-down lists, and specific changes in the UI that have to be triggered by specific actions.

My plan was to make Elementary meet the following criteria:

  • It had to be a comprehensive solution that clearly divides the layers based on their responsibilities,
  • all layers had to be as isolated and independent from each other as possible,
  • the solution had to be testable,
  • the implementation had to be as close to the essence of Flutter as possible.

All of these constraints, combined with Flutter’s unique characteristics, fell in line with a classic pattern called Model-View-ViewModel (MVVM) → Model-Widget-WidgetModel (MWWM). That is where the story of Elementary began.

MVVM — is a design pattern for app architecture.

MWWM — is a Model-View-ViewModel pattern architecture and implementation that our team ported to Flutter.

When our Flutter team split off from the Android team, we already had some experience implementing it. MVVM was a smart choice, and that is how we ended up with MWWM. We did not know the framework inside and out back when the Flutter team and Flutter itself were just getting started. As a result, the library not only had its strengths, but also its flaws.

I wanted to provide developers with a tool that would be easy to use, flexible, dependable, and testable. Back when we were implementing MVVM, we used to run into some issues, so I wanted Elementary to inherit the best parts of MVVM while avoiding some of its drawbacks.

The goal of this library, as well as the pattern itself, is to separate code into three layers of responsibility: user interface, business logic, and presentation logic. As a result, we have well-structured, self-contained modules. To get the gist of it, all you have to know are these three entities:

  • elementaryWidget is where the screen layout is put together.
  • widgetModel (WM) contains presentation logic.
  • elementaryModel contains business logic and component logic.
Graphical representation of interactions between components in Elementary

The package is available at pub.dev. You can check out the source code on GitHub.

The solution

Elementary under the hood

Elementary is a library that allows developers to write apps using the principles of Clean Architecture, in which modules are divided into blocks. It is a Flutter architectural package that lets developers clearly separate app layers based on their responsibilities, making the latter more transparent and the code more readable and testable. 

The library consists of several core modules, each neatly organized.

ElementaryModel

WidgetModel corresponds to the ViewModel from MVVM. Similarly to MVVM, WidgetModel is a model state adapter. Therefore, ElementaryModel is the direct dependency of WidgetModel, allowing us to communicate with business logic. It is the only entry point for presentation logic to interact with business logic.

WidgetModel is where you must encapsulate all the presentation logic.

WidgetModel

WidgetModel corresponds to the ViewModel from MVVM. Similarly to MVVM, WidgetModel is a model state adapter. Therefore, ElementaryModel is the direct dependency of WidgetModel, allowing us to communicate with business logic. It is the only entry point for presentation logic to interact with business logic.

WidgetModel is where you must encapsulate all the presentation logic. 

ElementaryWidget

ElementaryWidget is a presentation layer corresponding to View from MVVM. One type of widget in Flutter is layout widgets, such as Stateless and Stateful widgets. These widgets don’t display anything per se; instead, they take data and put it together (like Lego bricks) to display. As a result, ElementaryWidget evolved into a layout layer with its own implementation. 

To help layout widgets understand where they can build their own part of a subtree, they are normally passed BuildContext. Judging by the concept of MVVM, the only source of data is WidgetModel. This makes the presentation layer extremely simple. Its sole purpose is to define the current presentation based on the properties provided. Moreover, each presentation component of this kind can be separated from the rest. After all, the only thing is needs is WidgetModel.

A change in a property should lead to an update in a specific part of UI. That’s why for properties like StateNotifier we added builders that rely on their value. If WidgetModel is well thought out, such an approach can offer a great boost in performance: we only update the specific parts leaving out massive layout fragments that didn’t need to be changed.

Element

For all of it to work properly, we need a mechanism that binds all the layers together. Element does just that in Flutter. Developers don’t need to handle Element directly. However, this mechanism is critical, so much so that is got pretty much the same name as the library — Elementary.

Element:

  • stores WidgetModel,
  • ensures it is operational by managing its lifecycle,
  • provides it to a widget in the form of a contract so that the widget can present it.

As a result, the structure of each binding, including the Element, looks roughly like this:

Handling errors in Elementary

Errors are usually handled from two perspectives: that of a developer and that of a user. 

  • Developers need to have information on the errors as well as data on what happened and where it happened. That way, they can quickly figure out what went wrong and fix it.
  • Users need to have their usual non-traumatic experience even if there is an error. 

In Elementary, both of them are covered.

To handle all errors in a centralized manner, ElementaryModel is passed an entity called ErrorHandler. In it, you can implement logging or handle the error in any other way according to business logic. The error is also reported to WidgetModel so that it can be handled from the user’s perspective: e.g. show a snack bar — a short message at the top of the interface that provides immediate feedback to a specific action.

WidgetModel has a lifecycle method — onErrorHandle — to do just that. Calling handleError launches the entire mechanism handling an error inside a model.

As a result, the process of handling an error may look as follows:

Testability in Elementary

One of the things that Elementary had to ensure was testability. We were striving to make all three layers testable.

ElementaryModel can be covered with unit tests that check specific modules of the system. That’s pretty straightforward: business layer dependencies are passed to a class and can be mocked, meaning you can send requests to a module stub instead of an actual server.

ElementaryWidget can be tested with both the Widget and Golden tests. The process of testing becomes even easier than it was in the standard Flutter approach: the widget’s sole source of data is WidgetModel, which is presented as an interface and contains properties, while the widget itself is just a wrapper over a subtree.

Testing WidgetModel wasn’t that easy at first. We needed to verify that, under specific conditions, WidgetModel provides properties as expected. However, there was no practical way of testing it: all the tests ended up being pretty lengthy. 

Therefore, we wrote a library called elementary test. It does the work of imitating the software component all by itself and only shows you the “remote control” to manage the processes. Now, testing WidgetModel is easy.

An app built with Elementary can easily be covered with all kinds of tests — all thanks to dividing an app into layers and getting rid of tight coupling between them.

The result

With Elementary, you can write cleaner code and cut back on maintenance and revision times. Thanks to this library, we spend less time on development, maintenance, and upgrades in the long run. Moreover, it is easy to test; finding and fixing a bug takes much less time. Besides, the tests themselves are easier to write, which in turn guarantees code quality and stability in case the code needs to be modified.

Contact Us

Fill the form and get an estimate of your project
Drop a file here or click to upload Attach file
    Hidden span
    Estimate project