Flutter provides a modern react-style framework, rich widget collection and tooling, but there’s nothing similar to Android’s guide to app architecture.

Indeed, there’s no ultimate architecture that would meet all the possible requirements, yet let’s face the fact that most of the mobile apps we are working on have at least some of the following functionality:

Request/upload data from/to the network. Map, transform, prepare data and present it to the user. Put/get data to/from the database.

Taking this into account I have created a sample app that is solving the same problem using three different approaches to the architecture.

User is presented with a button “Load user data” in the centre of the screen. When the user clicks the button asynchronous data loading is triggered and the button is replaced with a loading indicator. After data is loaded loading indicator is replaced with the data.

Let’s get started.

Data

For simplicity, I have created the Repository class that contains the getUser() method that emulates an asynchronous network call and returns Future<User> object with hardcoded values.

If you are not familiar with Futures and asynchronous programming in Dart you can learn more about it by following this tutorial and reading a doc.

Vanilla

Let’s build the app in the way most developers would do after reading official Flutter documentation.

Navigating to VanillaScreen screen using Navigator

As the state of the widget could change several times during the lifetime of the widget we should extend StatefulWidget . Implementing a stateful widget also requires to have a State class. Fields bool _isLoading and User _user in _VanillaScreenState class represent the state of the widget. Both fields are initialised before the build(BuildContext context) method is called.

When the widget state object is created the build(BuildContext context) method is called to build the UI. All the decisions about the widgets that should be built to represent the current state are made in the UI declaration code.

body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), )

In order do display progress indicator when the user clicks “Load user details” button we do following.

setState(() { _isLoading = true; });

Calling setState() notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a build for this State object.

That means that after calling the setState() method the build(BuildContext context) method is called by the framework again and the whole widget tree is rebuilt. As _isLoading is now set to true method _buildLoading() is called instead of _buildBody() and the loading indicator is displayed on the screen. The same happens when we handle the callback from getUser() and call setState() to reassign _isLoading and _user fields.

widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); });

Pros

Easy to learn and understand. No third-party libraries are required.

Cons

The whole widget tree is rebuilt every time widget state changes. It’s breaking the single responsibility principle. Widget is not only responsible for building the UI, but it’s also responsible for data loading, business logic and state management. Decisions about how the current state should be represented are made in the UI declaration code. If we would have a bit more complex state code readability would decrease.

Scoped Model

Scoped Model is a third-party package that is not included in Flutter framework. This is how the developers of Scoped Model describe it:

A set of utilities that allow you to easily pass a data Model from a parent Widget down to its descendants. In addition, it also rebuilds all of the children that use the model when the model is updated. This library was originally extracted from the Fuchsia codebase.

Let’s build the same screen using Scoped Model. First, we need to install the Scoped Model package by adding scoped_model dependency to pubspec.yaml under dependencies section.

scoped_model: ^1.0.1

Let’s take a look at UserModelScreen widget and compare it with the previous example that was built without using Scoped Model. As we want to make our model available to all the widget’s descendants we should wrap it with generic ScopedModel and provide a widget and a model.

In the previous example, the whole widget tree was rebuilt when the widget’s state changed. But do we require to rebuild the whole screen? For example, AppBar shouldn’t change at all so there’s no point in rebuilding it. Ideally, we should rebuild only those widgets that are updated. Scoped Model can help us to solve that.

ScopedModelDescendant<UserModel> widget is used to find UserModel in the Widget tree. It will be automatically rebuilt whenever the UserModel notifies that change has taken place.

Another improvement is that UserModelScreen is not anymore responsible for state management and business logic.

Let’s take a look at UserModel code.

Now UserModel holds and manages the state. To notify listeners (and rebuild descendants) that the change took place notifyListeners() method should be called.

Pros

Business logic, state management and UI code separation. Easy to learn.

Cons

Requires third-party library. As the model gets more and more complex it’s hard to keep track when you should call notifyListeners() .

BLoC

BLoC (Business Logic Components) is a pattern recommended by Google developers. It leverages streams functionality to manage and propagate state changes.

For Android developers: You can think of Bloc object as a ViewModel and of StreamController as a LiveData . This will make the following code very straightforward as you’re already familiar with the concepts.

No additional method calls are required to notify subscribers when the state changes.

I have created 3 classes to represent possible states of the screen:

UserInitState for the state, when the user opens a screen with a button in the centre. UserLoadingState for the state, when loading indicator is displayed while data is being loaded. UserDataState for the state, when data is loaded and displayed on the screen.

Propagating state changes in this way allows us to get rid of all the logic in the UI declaration code. In example with Scoped Model, we still were checking if _isLoading is true in the UI declaration code to decide which widget we should render. In the case with BLoC, we are propagating the state of the screen and the only responsibility of UserBlocScreen widget is to render the UI for this state.

UserBlocScreen code got even simpler in comparison to the previous examples. To listen to the state changes we are using StreamBuilder . StreamBuilder is a StatefulWidget that builds itself based on the latest snapshot of interaction with a Stream .

Pros

No third-party libraries needed. Business logic, state management and UI logic separation. It’s reactive. No additional calls are needed like in the case with Scoped Model’s notifyListeners() .

Cons

Experience working with streams or rxdart is required.

Source Code

You can check out the source code of the examples above form this github repo.