Model-Widget-WidgetModel, or Surf’s Flutter Team’s App Architecture of Choice

Артем Зайцев
Surf
Published in
11 min readJul 30, 2020

Hi, my name is Artem and I am the Head of Flutter Development Team at Surf and a co-host of the FlutterDev podcast.

Our Flutter department has existed for over a year now. During this time, we completed several projects: from small corporate to full-fledged e-commerce and banking applications for some of the largest Russian companies. In this article, we will take a look at a recently released mwwm package which is an architecture we use in all our Flutter projects.

What is MWWM?

MWWM is an architecture type and an implementation of the Model-View-ViewModel pattern which our team adapted for Flutter. We replaced the word View with Widget to make things clearer for the devs as View is not used in Flutter very often. The main advantage of this architecture is that it allows you to separate layout and logic, both business logic and logic of the presentation layer.

How we came up with MWWM

Let us go back to the beginning of 2019 when our Flutter department was first founded. It has been a month since Flutter was released and everyone was hyped about it. At the same time, there were no active players on the market as well as in the open source community. A perfect time to storm into the industry and claim a good place for yourself, right? The framework is young, the community is growing opening up all roads.

This is when the Flutter department becomes separate from the Android department. The immediate task was to lay the foundation for our future apps and then start coding. Sure enough, choosing an architecture was one of the key moments.

In early 2019, there was no set opinion in the community on which architecture to choose — although it has become quite a heated debate at the moment. There are several basic concepts: BLoC, Redux, Vanilla, MobX etc. BLoC and Redux are most popular among them. Note that we are talking about the concept, not packages per se.

We had to decide which one to choose, BLoC or Redux, or maybe come up with our own creative solution.

Why we decided against BLoC

Business Logic Component is a great concept. Blocks of code which are basically black boxes with some input and output flow containing business logic written in pure Dart — it sounds amazing. Pure Dart for cross-platform development allows to reuse the code for web, and this was possible way before Flutter for Web was released, back when websites were written in Angular Dart. All things considered, BLoC is easy to use and offers sufficient block isolation. But there is one catch. Where do you write the logic of the presentation layer? Where do you write navigation? How do you work with UI events?

Later on, the Bloc package by Felix Angelov was released along with flutter_bloc. Developers started considering blocks as logic of the presentation layer but it no longer fit the acronym. The library did not give instructions on where to put this logic, for instance, where to validate input fields. This was unacceptable for us.

What about Redux?

Like I said, our Flutter team comes from Android development. At the time, the world of development was dominated by the concept of clean architecture and MVVM seemed to be a good option. Web technologies were considered somewhat exotic. We quickly ruled out Redux due to a high entry threshold. Because, otherwise, we would have to change the mindset we got used to during Android development using Rx and CleanArchitecture.

We decided to shift away from the basic concepts and focus on the specifics of our company instead. We set the goal of creating an architecture that would allow our Android developers to quickly convert into Flutter developers if the need arises — at the same time allowing our Flutter devs to switch to Android. They would already be familiar with the architecture and the language is fairly easy to learn. This is how Model — Widget — WidgetModel came into being.

Model-Widget-WidgetModel

Here is what it looks like.

You can find this illustration on the GitHub page. It consists of several major parts:

  • Widget — passive UI.
  • WidgetModel is the logic of the UI and its state.
  • Model is the contract with the service layer. This is an experimental part at the moment. We previously used Interactors directly in WM.

Let us take a closer look at each of the parts.

Widget to be a completely passive layout in MWWM. It may, at best, have a conditional statement when we write if (condition) then show me data, otherwise show loader. The rest (calculating these conditions, processing taps and so on) goes to WidgetModel. On top of that, a widget can represent either a whole screen or a specific element on screen along with its state.

It is worth pointing out that we also use regular Flutter widgets alongside MwwmWidget. Regular widgets are indispensable. Sometimes there is no need to complicate things to make a simple switch for the sake of architecture.

WidgetModel is essentially the state of a widget and a description of how it works. The logic is responsible for processing UI actions, calling other layers that are higher up, calculating values, data handling and mapping of data required for layout — everything a standard ViewModel is in charge of.

Consider this example.

Imagine a screen with a certain state and a button that requests the server. The screen widget will only contain the layout. At the same time, its state and reaction to actions are located in a WidgetModel. Note that WM also describes some of the microstates on screen. These microstates can also be Streams.

class SomeWidgetModel{
final _itemsController = StreamController<List<Items>>.broadcast();
get items => itemsController.stream;}

Our team uses streams inside widget models, namely stream wrappers. This makes widget models similar to the concept of BLoC. A widget model is essentially a box containing input/output. Every input is a user action or an event whereas output stands for data that affect the UI. Each stream is a microstate inside the widget model. These states can be represented by small elements such as a button that gets disabled under certain conditions or a stream of text displayed on screen etc.

Speaking of the button. Its state can be one of such streams meaning that it will be wrapped in StreamBuilder when displayed on screen. You have to understand that the button’s logic needs to be quite simple if this is the case.

Stream<bool> get isBtnDisabled => btnController.stream;

Now, imagine that there are five more buttons like this present in the app. After some changes of the state to, say, disable, the button triggers a network request, provided that it is the same request with different arguments. In this case, it would not be smart to copy and paste the logic from one screen to another and make a mess in the widget model. A better approach would be to simply select this button in Widget+WidgetModel and reuse it in other screens while passing certain parameters for input.

Another important point is that using a widget model is the only way to bring a widget or a part of it to a certain state. It means that in Flutter, we can pass certain widget arguments to the constructor. This comes in handy if you use stateless widgets in your project.

What happens if we use a WM? State of a widget can only be handled by a widget model which makes it the main source. If we pass data to a widget, we also have to pass it to a WM and only then extract data from it. You cannot use these data in widgets directly because it would create two ways of bringing widgets to a desired state. This is not only confusing but it could potentially lead to some nasty bugs that would be hard to track.

W-WM data flow

Let us look at the data flow between widgets and widget models. Some actions such as UI actions come from widgets and some states are initiated in widget models. States can be represented as variables or as streams like we do at Surf. We subscribe parts of a widget to these states by using StreamBuilder.

//…child: StreamBuilder<Item>(stream: wm.item,builder: (ctx, snapshot) => //…),

Note that the connection between Widget and WidgetModel is not written in the package explicitly. We decided not to narrow the framework’s capabilities down and gave users the freedom to define the connection manually instead. The approach that we follow at Surf is available as a separate package on pub.dev.

Relation

We recommend using our MWWM package together with the Relation module. Relation is the type of connection between widgets and widget models. This is merely a semantic wrapper around streams. We also have some streamed states in the form of StreamedState and actions (Action). Relation is fairly easy to work with.

final toggleAction = Action<int>();final contentState = StreamedState<int>(0);//…subscribe(toggleAction.stream, (data) => contentState.accept(data));

Error handling

Large projects require errors to be handled correctly. We added a special interface to MWWM called ErrorHandler which is available in WM. WidgetModel intercepts errors coming from the service layer (or happening in the presentation layer) and passes them on to the handler. Handling happens automatically when using the WM methods with the …HandleError() postfix.

subscribeHandleError(someAction, (data) => doOnData());doFutureHandleError(someFuture, (data) => doOnData());

You will find the implementation of the ErrorHandler in the sample project.

Model

Model is an experimental feature that remains an optional part of the architecture for the time being. This is a description of the contract between a widget model and the business logic of an app.

Model is both a contract and a unified API responsible for interacting with the service layer. It is in fact an entity that contains two methods: perform and listen. At the same time, it receives a list of Performers but we will get to that in a moment.

The interaction looks like this. A WM says, “Hey, Model, I want to make a Change (perform an action) to the service layer. Do it.” And waits for the result. This design allows them to completely abstract from one another by agreeing on the contract.

Change

The Change class is the first part of the contract that describes the intent to change something or get some data as well as the result type. It can contain data but it cannot contain any logic. This is a lot like the data class in Kotlin.

class Authenticate extends FutureChange<Result> {final String name;Authenticate(this.name);}

Performer

The second part of the contract is Performer. It implements logic. The closest alternative to the performer is UseCase. This is the functional part of the contract. If Change is the name of a method and a class that represents parameters, then Performer is a method body which is basically the code that is being executed by Change. Any services, business logic and interactors can be delivered to the performer. Such a design allows you to completely unlink the widget model from the business logic implementation.

A perfect performer would consist of only one action. It is a very independent piece of code that performs just one task and outputs a result. Performer is uniquely linked to the Change type.

class AuthPerformer extends FuturePerformer<Result, Authenticate> {final AuthService authService;AuthPerformer(// entities required by the performerthis.authService,);Future<Result> perform(Authenticate change) {return authService.login(change.name);}}

One method turns into two classes

Why separate it? If the interactor only contains one method, this way we get two classes instead. But this means losing data related to interactors as well as service layer implementation from the widget model. You just have to know what you want to do, meaning Change, and provide Model with a necessary set of performers.

But there is a downside to it. If you did not provide a Performer, you will only find out about it in runtime.

Business logic

You are given a complete discretion here. MWWM does not tell you how to implement the business logic of your app. We advise using the approach adopted in your team. At Surf, we use CleanArchitecture. In my pet project, I use services that are delivered to performers and work inside of them. You can do anything here. The whole point of MWWM is to be flexible and easy to adapt for any team.

Surf’s tech stack

Our stack is essentially a combination of architecture packages combined in a single package called surf_mwwm. Go to our GitHub page to find out more about it.

The package contents are shown in the diagram above:

  • injector — a package for implementing DI built upon InheritedWidget. It is small and easy to use
  • relation — the connection between layers
  • mwwm — the subject matter of this article
  • surf_mwwm — combination of these components with a small team-specific add-on

Summary

I hope that this approach along with the package will come in handy for teams, especially large ones. Establishing contracts between layers allows you to develop several features simultaneously and avoid colliding on the same files making it possible for developers to work autonomously.

It may seem like an overhead to someone who works on a project alone as it slows down development. True, you can write much faster in other frameworks at the cost of flexibility and customization. MWWM gives you more freedom, you can always adapt the architecture to your needs. The biggest advantage our architecture provides is the ability to separate all layers which improves readability and makes it easier to support the code.

Let us sum up.

Pros of MWWM:

  • there are no unnecessary dependencies
  • you can separate and isolate all layers (UI, Presentation Logic, Business Logic)
  • you can configure it to suit your team’s needs
  • you can establish contracts and work on several features at once

Cons:

  • it is too complex to be used in projects led by solo developers
  • the contract description is quite lengthy

MWWM is a part of a larger project with its own separate repository called SurfGear. It is a set of standards and libraries that we use at Surf.

Some of these libraries have already been released on pub.dev:

We promise there is more to come. Stay tuned!

If you have a question — contact me in Twitter or write issue on GitHub.

--

--