BLoC - ScopedModel - Redux - Comparison

Compatibility
Last Reviewed
Mar 30, 2023
Published on
Apr 06, 2019
Flutter
v 3.13.x
Dart
v 3.1.x

Introduction

BLoC, ScopedModel, Redux... differences, when to be used, when NOT to be used, advantages, disadvantages...

Many questions frequently asked on this topic and so many answers can be found on the Internet but is there any right choice?

In order to provide my own analysis, I have considered 2 distinct types of use-cases, built a quick solution to cover these use-cases using the 3 frameworks and compared them.

The full source code that covers Redux, ScopedModel and BLoC solutions can be found on GitHub.


Part 1: What are they?

In order to better understand the differences, I think it might be useful to quickly remind their main principles.

Redux

Introduction

Redux is an Application State Management framework. In other words, its main objective is to manage a State.

Redux is architectured around the following principles:

  • Unidirectional data flow

  • One Store

    A Store acts like the orchestrator of Redux. The Store:

    • stores only one State
    • exposes one entry point, called dispatch which only accepts Actions in arguments
    • exposes one getter to fetch the current State
    • allows to (un-)register to be notified (via StreamSubscription) of any changes applied to the State
    • dispatches the actions and the store to the first MiddleWare
    • dispatches the actions and the current state to a Reducer (which might be a façade for several reducers)
  • Actions

    Actions are the only types of input accepted by the Store access point.Actions, combined with the current State are used by the Middleware(s) and Reducer to process some function, which could lead to amending the State.

    Actions only describe what happened

  • MiddleWare

    A Middleware is a function commonly aimed at running asynchronously (but not necessarily), based on an Action. A Middleware simply uses a State (or an Action as a trigger) but does not change the State.

  • Reducers

    A Reducer is normally a synchronous function which does some processing based on the combination Action - State. The outcome of the processing might lead to a new State.
    The Reducer is the only one allowed to change the State.

It is important to note that, according to Redux recommendations and good practices, there is only one single Store per application. To split the data handling logic, it is advised to use 'reducer composition' instead of many stores.

How does it work?

The following animation shows how Redux works:



Explanation:

  • When something happens at the UI level (but not limited to the UI, in fact), an Action is created and sent to the Store (via store.dispatch(action));
  • If one or several Middlewares are configured, they are invoked in sequence, passing them the Action and a reference to the Store (so, also the State, indirectly);
  • Middlewares could themselves send an Action to the Store during their processing;
  • Then the Action and the current State are also sent to the Reducer
  • The Reducer is the only one that will potentially change the State
  • When the State is changed, the Store notifies all registered listeners to inform them.
  • The UI (but not limited to the UI) can then take appropriate actions linked to change of State.

Implementations

Redux has intially been developed for Javascript and ported to Dart as a package.

A Flutter dedicated package (flutter_redux) provides some Widgets, such as:

  • StoreProvider to pass the Store to all descendant Widgets
  • StoreBuilder to get the Store from a StoreProvider and pass it to a Widget builder function
  • StoreConnector to get the Store from the nearest StoreProvider ancestor, convert it into a ViewModel and pass it to a builder function.

ScopedModel

Introduction

ScopedModel is a set of utilities to allow to pass a data Model from a parent Widget down it its descendants.

ScopedModel is articulated around 3 classes:

  • Model

    A Model is a class that holds the data and business logic related to the data. It is implemented as a Listenable and can notify whoever might be interested in knowing when a change applies.

  • ScopedModel

    ScopedModel is a Widget, similar to a Provider, which 'holds' the Model and allows:

    • the retrieval of the Model, via the usual ScopedModel.of<Model>(context) call
    • the registration of the context as a dependency of the underlying InheritedWidget, when requested.

      The ScopedModel is based on an AnimatedBuilder which listens to notifications sent by the Model and then rebuilds an InheritedWidget which, in turn, will request all the dependencies to rebuild.

  • ScopedModelDescendant

    ScopedModelDescendant is a Widget which reacts on variations of the Model; it rebuilds when the Model notifies that a change has taken place.

How does it work?

The following code extract mimics the "counter" application, using Scoped Model.



The following animation shows what happens when the user taps on the Button.



Explanation:

  • When the user taps the RaisedButton, the model.increment() method is invoked
  • This method simply increments the counter value and then invokes the notifyListeners() API, available since the Model implements the Listenable abstracts class.
  • This notifyListeners() is intercepted by the AnimatedBuilder, which rebuilds its InheritedWidget child
  • The InheritedWidget inserts the Column and its children
  • One of the children is the ScopedModelDescendant which invokes its builder that refers to the Model which now holds a new counter value.

BLoC

Introduction

The BLoC pattern does not require any external library or package as it simply relies on the use of the Streams. However, for more friendly features (e.g. Subject), it is very often combined with the RxDart package.

The BLoC pattern relies on:

  • StreamController

    A StreamController exposes a StreamSink to inject data in the Stream and a Stream to listen to data, flowing inside the Stream.

  • StreamBuilder

    A StreamBuilder is a Widget which listens to a stream and rebuilds when new data is emitted by the Stream.

  • StreamSubscription

    A StreamSubscription allows to listen to data being emitted by a stream and react.

  • BlocProvider

    A BlocProvider is a convenient Widget, commonly used to hold a BLoC and make it available to descendant Widgets.

How does it work?

The following animation shows how the BLoC works when we are injecting some data into one of its Sinks or via an API.
(I know, BLoC are only supposed to be used with Sinks and Streams... but nothing really prevents from also using an API...)



Explanation:

  • Some data is injected into one of the BLoC sinks
  • The data is processed by the BLoC which eventually emits some data from one of the output streams
  • The very same can also apply when using the BLoC API...

For additional information on the notion of BLoC, please refer to my 2 articles on the topic:


Part 2

Now that we have a better idea of what they are and work, let's compare them...

To do this, I will take 2 samples to illustrate their differences, advantages, and disadvantages...

Case 1: User Authentication

This common use-case is very interesting since it involves some type of Application State. In this example, I want the page to act as follows:



  • a text that shows if the user is not authenticated and a button to simulate an authentication;
  • a CircularProgressIndicator when a simulated authentication process is on-going;
  • the first name and last name of the authenticated user, together with a button to log out.

Code Comparison

The following 2 pictures show, side-by-side, the codes related to the initialization of the application and the page, respectively.





As we can see, there is not that much difference.

This absence of big difference might be the result of the architectural decision I made, as I took as a requirement that we could need to able to "log out" from anywhere in the application. Therefore, I needed both ScopedModel and BLoC solutions to inject their respective model and bloc on top of the MaterialApp, to be available from anywhere later on.

However, there are differences... let's check them...


Differences and Observations

Number of files

In Redux, the solution leads to many more files (if we want to stick to the "one entity, one file" paradigm). Even if we regroup the entities based on their responsibilities (e.g. put all actions together), we still have more files.

The ScopedModel solution requires fewer files as the model holds both the data and the logic.

The BLoC solution requires one additional file, compared to the ScopedModel, if we want to split both model and logic, but this is not compulsory.

Code execution

In Redux, because of its way of running, we have much more code that is executed, sometimes for nothing. Indeed, the way to write a reducer is based on condition evaluations such as: "if action is ... then", and the same applies to the middlewares.

Also, and maybe because of the implementation made by the flutter_redux package, a StoreConnector requires a converter, which sometimes is not necessary. This converter is meant to provide a way of producing a ViewModel.

Both ScopedModel and BLoC solutions seem to be the ones which require less code execution.

Code complexity

If you keep in mind that it is always an Action that triggers all middlewares to be run in sequence (up to the moment they implement something asynchronous), then the reducer which needs to do things based on a comparison on an action type, the code is relatively simple. However, very soon it will require to use the notion of reducer composition (see combineReducers with TypedReducer).

The ScopedModel solution looks like the one which leads to the simplest code: you call a method which updates the model that notifies the listeners. However, it is not obvious for the listeners to know the reason that led being notified since any modification to the model generates notifications (even if it is of no interest for that particular listener).

The BLoC solution is a bit more complex as it involves the notion of Streams.

Behind the scene, the flutter_redux solution also relies on the use of Streams, but this is hidden from a developer perspective.

Number of (re-)Builds

If we have a look at the number of times parts of the application rebuild, it becomes interesting...

As internally, the flutter_redux uses the notion of Streams to react on changes applied to the State, if you do not try to get access to the Store via the StoreProvider.of(context) API, only the StoreConnector will rebuild (as implemented using the StreamBuilder). This makes the flutter_redux implementation interesting from a rebuild perspective.

The ScopedModel solution is the one that produces the more builds since each time a Model notifies its listeners, it rebuilds the whole tree under the ScopedModel() widget (in fact under the underlying AnimatedBuilder).

The BLoC solution, as ideally based on StreamBuilder to respond to changes is, with flutter_redux the one which causes the less builds (only the part related to StreamBuilder is rebuilt).

Code Isolation

In Redux, reducers and middlewares are, "usually" but not necessarily, top-level functions/methods meaning not part of a class. As a consequence, nothing would prevent calling them outside the scope of the Redux Store, which would not be ideal.

Both ScopedModel and BLoC tend to prone code isolation: one specific class for the model or the bloc.


Case 1: Conclusions

For this specific case and because there is no constraints of performance (rebuilds), I personally don't see any benefits of opting for one or another solution.

The little advantage I would give to Redux is the ability to insert a middleware to log the different actions: you simply need to add the reference of the middleware method when initializing the Store.

For both ScopedModel and BLoC, this would require much effort.


Case 2:

For this case, we are going to simulate some kind of dashboard where the user may dynamically add new panels. Each panel simulates some real-time data evolution (e.g. stock exchange). The user may turn the real-time data fetching on/off for each of these panels, individually.

Here is what the application looks like:




Code Comparison

As I wanted to stick to the Redux main principle (one Store per application), it was harder to implement this in Redux since the ApplicationState required to remember and handle each individual Panels.

Now, if you do not want to stick to this principle, nothing actually prevents you from using multiple Stores, one per Panel. Proceeding that way, the code is a bit simpler.

As regards the ScopedModel and BLoC versions, the codes are very similar.


Differences and Observations

Number of files

Here again, Redux requires more files (even if we regroup as much as possible) than for the other 2 solutions.

For this case, both ScopedModel and BLoC solutions require the same amount of files.

Code execution

The very same comments as for case 1.

Redux executes much more code than both ScopedModel and BLoC solutions as the reducer is based on condition evaluations such as: "if action is ... then", and the same applies to the middlewares. In addition, 3 instances of StoreConnector are necessary:

  • at the Page level to add a new Panel
  • at the Widget level to fetch the stats
  • at the Widget level to handle the button that turns the stats on/off.

ScopedModel requires additional code execution than BLoC as ScopedModel relies on Listenable/InheritedWidget to rebuild each time the Model changes.

The solution based on ScopedModel requires, per Panel:

  • one ScopedModel (injector)
  • one ScopedModelDescendant to handle the display of the stats
  • one ScopedModelDescendant to handle the button that turns the stats on/off

BLoC is the one which executes the less code. Per Panel, the solution requires:

  • one StreamBuilder to display the stats
  • one StreamBuilder to handle the button that turns the stats on/off
Code complexity

The Redux solution is more complex as it requires the dispatching of Actions at 3 different places:

  • at the Page level, when we need to instantiate a new Panel
  • at the Widget level, when we need to switch the timer on/off via the button
  • at the Middleware level, when a new stats value is obtained from the server

The complexity of both ScopedModel and BLoC solutions is only located at the Model and BLoC levels, respectively. As each Panel has its own Model or BLoC, the code is less complex.

Number of (re-)Builds

The Redux solution is the one that causes the most of rebuilds.

In the implementation, based on "one Store per Application", each time a change applies to the ApplicationState, everything is rebuilt, meaning:

  • when we add a new Panel
  • when we turn on/off the collection of stats, all Panels are rebuilt
  • when a new value is added to one Panel, all Panels are rebuilt

If I had chosen to have one Store per Panel, the number of (re-)builds would have been much lower.

As regards the ScopedModel solution, the number of rebuilds is a bit more limited:

  • when we add a new Panel
  • limited to one Panel, when
    • we turn the collection of stats on/off
    • a new value is collected for a specific Panel

Finally, the BLoC solution is the one that requires fewer rebuilds:

  • when we add a new Panel
  • when we turn the collection of stats on/off, only the button related to the specific panel is rebuilt
  • when a new value is collected for a specific Panel, only the Panel is rebuilt.

Case 2: Conclusions

For this specific case, I personally find that the BLoC solution is the best option from both code complexity and rebuilds perspectives.

The ScopedModel solution comes next.

The Redux architecture is not optimal for this solution, but it is still possible to use it.


Other packages

Before talking about any conclusions, I wanted to mention that several additional packages exist today around the notion of Redux, among which the following 2 ones might be interesting for those who still prefer Redux:

  • rebloc, which combines aspects of Redux and BLoC

    This package is quite interesting but still does not solve the overhead of code execution, linked to the notion of reducer (if action is ... then). However, it is worth having a look at it.

  • fish_redux, from the Alibaba Xianyu team.

    This package is not a State Management framework but rather an Application Framework, based on Redux.
    This package is very interesting but requires to totally change the way of developing an application. This solution makes it easier to structure the code in terms of Actions and Reducers


Conclusions

This analysis allowed me to compare 3 of the most commonly used frameworks (or any other name we can use to refer to them), in their bare form, based 2 distinct use-cases.

These 3 frameworks have their pros and cons, which I list here below (This is my personal view, of course...):

Redux

  • Pros

    • Redux allows to centralize the management of a State thanks to the fact that Reducers are the only one(s) that can perform the transition from one state to another. This makes the state transition perfectly predictable and thoroughly testable.
    • The ease to insert middlewares in the flow is also an asset. If, for example, you need to constantly validate the connectivity with the server or trace the activities, this is a perfect placeholder for such routines.
    • It forces the developer to structure the application in terms of "Event -> Action -> Model -> ViewModel -> View".
  • Cons

    • One single Store and a huge State (if you want to stick to Redux good practices)
    • Use of top-level functions/methods
    • Too many "if ... then" comparisons at both reducers and middlewares levels
    • Too many rebuilds (each time there is a change in the State)
    • Requires the use of an external package with the risks that the package evolves with breaking changes.
  • To be used

    • I could recommend Redux when you need to deal with a global application state, such as, e.g., user authentication, shopping basket, preferences (language, currency)...
  • Not to be used

    • I would not recommend Redux when you need to handle multiple instances of something, each of them having its own State

Scoped Model

  • Pros

    • ScopedModel makes it very easy to regroup the Model and its logic in a single location.
    • ScopedModel does not require any knowledge of the notion of Streams, which makes it easier to implement for beginners.
    • ScopedModel could be used for both global and local logics
    • ScopedModel is not limited to State Management
  • Cons

    • ScopedModel does not provide any means to let the code know which part(s) of the Model changed and caused the ScopedModelDescendant to be invoked
    • Too many (re-)builds. Each time a Model notifies its listeners, everything related to that Model rebuilds (AnimatedBuilder, InheritedWidget...)
    • Requires the use of an external package with the risks that the package evolves with breaking changes.
  • To be used

    • When developers are not very familiar with Streams
    • When the Model is not too complex
  • Not to be used

    • When an application needs to reduce the number of builds, for performance reasons.
    • When an application needs to know precisely which part of the Model has changed

BLoC

  • Pros

    • BLoC makes it easy to regroup the Business Logic in a single location
    • BLoC makes it very easy to determine with precision the nature of any changes (through its output interface, based on Streams)
    • BLoC makes it very easy to limit the number of (re-)builds to the strict minimum, thanks to the use of the StreamBuilder Widget
    • The use of Streams is very powerful and opens the door to many actions (transform, distinct, debounce...)
    • BLoCs could be used for both global and local logics
    • BLoC is not limited to State Management
    • Does not require the use of any external package.
  • Cons

    • If you want to stick to the main rule, we can only deal with sinks and streams.
      Personally, my BLoCs also expose getters/setters/API, which removes this "cons".
    • It is harder for beginners to start with BLoC as it requires additional understanding on how Flutter actually works
  • To be used

    • I do not see any restriction.
  • Not to be used

    • I do not see any case where I would recommend not to use a BLoC, except when developers are not familiar with the notion of Streams.

Therefore, is there any 'one single perfect solution'?

In fact, I would say that there is no "one single" perfect solution. It really depends on your use case and, moreover, it really depends on which framework you are the most comfortable with.

As a conclusion, I will only speak about myself...

I have never used Redux so far in any of my projects and I have never had the feeling that I missed anything. The same applies to ScopedModel...

Several months ago, I started up working with BLoC and I use this notion everywhere for almost everything, it is so convenient. It really made my code much cleaner, easier to test and much more structured and re-usable.

I hope this article has given you some additional insights...

Stay tuned for new articles, soon and meanwhile, let me wish you a happy coding!

0 Comments
Be the first to give a comment...
© 2024 - Flutteris
email: info@flutteris.com

Flutteris



Where excellence meets innovation
"your satisfaction is our priority"

© 2024 - Flutteris
email: info@flutteris.com