Flutteris

Last Reviewed
Published on
Invalid Date

Introduction

It took me quite a while to find a way to introduce the notions of Reactive Programming, BLoC and Streams.

As this is something that can make a drastic change to the way to architecture an application, I wanted a practical example that shows that:

  • it is very possible not to use them but could sometime be much harder to code and less performant,
  • the benefits of using them, but also
  • the impacts of using them (positive and/or negative).

The practical example I made is a pseudo application which, in short, allows a user to view a list of movies from an online catalog, filter them by genre and release dates, mark/unmark them as favourites. Of course, everything is interactive, user actions can happen in different pages or inside a same one and have impacts on the visual aspects, real time.

This is an animation that shows this application.


Streams Application

As you landed into this page to get information about Reactive Programming, BLoC and Streams, I will first start with an introduction to them. Thereafter, I will show you how to implement and use them, in practice.

A complement of this article which gives some pratical use cases can be found following this link.


What is a Stream?

Introduction

In order to easily visualize the notion of Stream, simply consider a pipe with 2 ends, only one allowing to insert something into it. When you insert something into the pipe, it flows inside the pipe and goes out by the other end.

In Flutter,

  • the pipe is called a Stream
  • to control the Stream, we usually(*) use a StreamController
  • to insert something into the Stream, the StreamController exposes the "entrance", called a StreamSink, accessible via the sink property
  • the way out of the Stream, is exposed by the StreamController via the stream property

(*): I intentionally used the term "usually", since it is very possible not to use any StreamController. However, as you will read in this article, I will only make use of StreamControllers.

What can be conveyed by a Stream?

Everything and anything. From a value, an event, an object, a collection, a map, an error or even another Stream, any type of data may be conveyed by a Stream.

How do I know that something is conveyed by a Stream?

When you need to be notified that something is conveyed by a Stream, you simply need to listen to the stream property of the StreamController.

When you define a listener, you receive a StreamSubscription object. This is via that StreamSubscription object that you will be notified that something happens at the level of the Stream.

As soon as there is at least one active listener, the Stream starts generating events to notify the active StreamSubscription object(s) each time:

  • some data goes out from the stream,
  • when some error has been sent to the stream,
  • when the stream is closed.

The StreamSubscription object also allows you to:

  • stop listening,
  • pause,
  • resume.

Is a Stream only a simple pipe?

No, a Stream also allows to process the data that flows inside it before it goes out.

To control the processing of the data inside a Stream, we use a StreamTransformer, which is nothing but

  • a function that "captures" the data that flows inside the Stream
  • does something with the data
  • the outcome of this transformation is also a Stream

You will directly understand from this statement that it is very possible to use several StreamTransformers in sequence.

A StreamTransformer may be used to do any type of processing, such as, e.g.:

  • filtering: to filter the data based on any type of condition,
  • regrouping: to regroup data,
  • modification: to apply any type of modification to the data,
  • inject data to other streams,
  • buffering,
  • processing: do any kind of action/operation based on the data,
  • ...

Types of Streams

There are 2 types of Streams.

Single-subscription Streams

This type of Stream only allows a single listener during the whole lifetime of that Stream.

It is not possible to listen twice on such Stream, even after the first subscription has been canceled.

Broadcast Streams

This second type of Stream allows any number of listeners.

It is possible to add a listener to a Broadcast Stream at any moment. The new listener will receive the events, as of the moment it starts listening to the Stream.

Basic Examples

Any type of data

This very first example shows a "Single-subscription" Stream, which simply prints the data which is input. As you may see the type of data does not matter.



StreamTransformer

This second example shows a "Broadcast" Stream, which conveys integer values and only prints the even numbers. To do so, we apply a StreamTransformer that filters (line #14) the values and only let the even numbers go through.



RxDart

Nowadays, the introduction to the Streams would no longer be complete if I would not mention the RxDart Package.

The RxDart package is an implementation for Dart of the ReactiveX API, which extends the original Dart Streams API to comply with the ReactiveX standards.

As it was not originally defined by Google, it uses a different vocabulary. The following table gives you the correlation between Dart and RxDart.

DartRxDart
StreamObservable
StreamController  Subject



RxDart as I just said, extends the original Dart Streams API and offers 3 main variations of the StreamController:

PublishSubject

The PublishSubject is a normal broadcast StreamController with one exception: stream returns an Subject rather than a Stream.


PublishSubject (c) ReactiveX.io

As you can see, PublishSubject sends to a listener only the events that are added to the Stream after the time of the subscription.

BehaviorSubject

The BehaviorSubject is also a broadcast StreamController which returns an Subject rather than a Stream.


BehaviorSubject (c) ReactiveX.io

The main difference with a PublishSubject is that the BehaviorSubject also sends the very last event that was emitted to the listener that just subscribed.

ReplaySubject

The ReplaySubject is also a broadcast StreamController which returns an Subject rather than a Stream.


ReplaySubject (c) ReactiveX.io

A ReplaySubject, by default, sends all the events that were already emitted by the Stream to any new listener as the very first events.


Important note about the Resources

It is a very good practice to always release the resources which are no longer necessary.

This statement applies to:

  • StreamSubscription - when you no longer need to listen to a stream, cancel the subscription;
  • StreamController - when you no longer need a StreamController, close it;
  • the same applies to RxDart Subjects, when you no longer need a BehaviourSubject, a PublishSubject..., close it.

How to build a Widget based on the data that goes out a Stream?

Flutter offers a very convenient StatefulWidget, called StreamBuilder.

A StreamBuilder listens to a Stream and, each time some data goes out that Stream, it automatically rebuilds, invoking its builder callback.

This is how to use the StreamBuilder:



StreamBuilder<T>(
  key: ...optional, the unique ID of this Widget...
  stream: ...the stream to listen to...
  initialData: ...any initial data, in case the stream would initially be empty...
  builder: (BuildContext context, AsyncSnapshot<T> snapshot){
      if (snapshot.hasData){
          return ...the Widget to be built based on snapshot.data
      }
      return ...the Widget to be built if no data is available
  },
)

The following example mimics the default "counter" application, but uses a Stream and no longer any setState.



Explanation and comments:

  • Lines #24-30: we are listening to a the stream, and each time a new value goes out this stream, we update the Text with that value;
  • Line #35: when we hit the FloatingActionButton, we increment the counter and send it to the Stream, via the sink; the fact of injecting a value in the stream causes the StreamBuilder that listens to it to rebuild and "refresh" the counter;
  • We no longer need the notion of State, everything is taken on board via the Stream;
  • This is a big improvement since the fact of calling the setState() method, forces the whole Widget (and any sub widgets) to rebuild. Here, ONLY the StreamBuilder is rebuilt (and of course its children widgets);
  • The only reason why we are still using a StatefulWidget for the page, is simply because we need to release the StreamController, via the dispose method, line #15;

What is Reactive Programming?

Reactive programming is programming with asynchronous data streams.

In other words, everything from an event (e.g. tap), changes on a variable, messages, ... to build requests, everything that may change or happen will be conveyed, triggered by a data stream.

In clear, all this means that, with reactive programming, the application:

  • becomes asynchronous,
  • is architectured around the notion of Streams and listeners,
  • when something happens somewhere (an event, a change of a variable ...) a notification is sent to a Stream,
  • if "somebody" listens to that Stream, it will be notified and will take appropriate action(s), whatever its location in the application.

There is no longer any tight coupling between components.

In short, when a Widget sends something to a Stream, that Widget does no longer need to know:

  • what is going to happen next,
  • who might use this information (no one, one or several Widgets...)
  • where this information might be used (nowhere, same screen, another one, several ones...),
  • when this information might be used (almost directly, after several seconds, never...).

...that Widget does only care about its own business, that's all !!

At first glance, reading this, this may seem to lead to a "no-control" of the application but, as we will see, this is the contrary. It gives you:

  • the opportunity to build parts of the application only responsible for specific activities,
  • to easily mock some components' behavior to allow more complete tests coverage,
  • to easily reuse components (somewhere else in the application or in another application),
  • to redesign the application and be able to move components from one place to another without too much refactoring,
  • ...

We will see the advantages shortly... but before I need to introduce the last topic: the BLoC Pattern.


The BLoC Pattern

The BLoC Pattern has been designed by Paolo Soares and Cong Hui, from Google and first presented during the DartConf 2018 (January 23-24, 2018). See the video on YouTube.

BLoC stands for Business Logic Component.

In short, the Business Logic needs to:

  • be moved to one or several BLoC s,
  • be removed as much as possible from the Presentation Layer. In other words, UI components should only worry about the UI things and not about the business,
  • rely on exclusive use of Streams for both input (Sink) and output (stream),
  • remain platform independent,
  • remain environment independent.

In fact, the BLoC pattern was initially conceived to allow to reuse the very same code independently of the platform: web application, mobile application, back-end.

What does it actually mean?

The BLoC pattern makes use of the notions that we just discussed above: Streams.




  • Widgets send events to the BLoC via Sinks,
  • Widgets are notified by the BLoC via streams,
  • the business logic which is implemented by the BLoC is none of their concern.

From this statement, we can directly see a huge benefit.

Thanks to the decoupling of the Business Logic from the UI:

  • we may change the Business Logic at any time, with a minimal impact on the application,
  • we may change the UI without any impact on the Business Logic,
  • it is now much easier to test the Business Logic.

How to apply this BLoC Pattern to the Counter Application sample?

Applying the BLoC pattern to this Counter Application might seem to be an overkill but let me first show you...



I already hear you saying "wow... why all this? Is this all necessary?".

First, the separation of responsibilities

If you check the CounterPage (lines 21 - 45), there is absolutely not any business logic inside it.

This page is now only responsible for:

  • displaying the counter, which is now only refreshed when necessary (even without the page having to know it)
  • providing a button which when hit, requests an action to be performed on the counter

Also, the whole the Business Logic is centralized in one single class "IncrementBloc".

If now, you need to change the business logic, you simply need to update the method _handleLogic (lines 72-75). Maybe the new business logic will request to do very complex things... The CounterPage will never know about it and this is very good!

Second, testability

It is now much easier to test the business logic.

No need anymore to test the business logic via the user interface. Only the IncrementBloc class needs to be tested.

Third, freedom to organize the layout

Thanks to the use of Streams, you can now organize the layout independently of the business logic.

Any action could be launched from any place in the application: simply call to the .incrementCounter sink.

You may display the counter anywhere, in any page, simply listen to the .outCounter stream.

Fourth, reduction of the number of "build"s

The fact of not using setState() but StreamBuilder, drastically reduces the amount of "build"s to only the ones which are required.

From a performance perspective, this is a huge improvement.


There is only 1 constraint... accessibility of the BLoC

In order to have all this working, the BLoC needs to be accessible.

There exists several ways of making it accessible:

  • via a global Singleton

    This way is very possible but not really recommended. Also as there is no class destructor in Dart, you will never be able to release the resources properly.

  • as a local instance

    You may instantiate a local instance of a BLoC. Under some circumstances, this solution perfectly fits some needs. In this case, you should always consider initializing inside a StatefulWidget so that you can take profit of the dispose() method to free it up.

  • provided by an ancestor

    The most common way of making it accessible is via an ancestor Widget, implemented as a StatefulWidget.

    The following code shows a sample of a generic BlocProvider.



Some explanations on this generic BlocProvider

First, how to use it as a provider?

If you have a look at the sample code "streams_4.dart", you will see the following lines of codes (lines #13-15)



home: BlocProvider<IncrementBloc>(
  bloc: IncrementBloc(),
  child: CounterPage(),
),

With these lines, we simply instantiate a new BlocProvider which will handle a IncrementBloc, and will render the CounterPage as a child.

From that moment on, any widget part of the sub-tree, starting at BlocProvider will be able to get access to the IncrementBloc, via the following line:



IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context)!;

Could we have multiple BLoCs?

Of course and this is highly advisable. Recommendations are:

  • (if there is any business logic) one BLoC on top of each page,
  • why not an ApplicationBloc to handle the Application State?
  • each "complex enough component" has a corresponding BLoC.

The following sample code shows an ApplicationBloc on top of the whole application, then the IncrementBloc on top of the CounterPage.

The sample also shows how to retrieve both blocs.



Why not using an InheritedWidget?

In most articles related to BLoC, you will see the implementation of the Provider as an InheritedWidget.

Of course, nothing prevents this type of implementation. However,

  • an InheritedWidget does not provide any dispose method and, remember, it is a very good practice to always release the resources when they are no longer needed;
  • of course, nothing would prevent you from wrapping the InheritedWidget inside another StatefulWidget, but then, what is the added value of using an InheritedWidget?
  • finally, the use of an InheritedWidget often leads to side effects if not controlled (see Reminder on InheritedWidget, below).

These 3 bullets explain the choice I made to implement the generic BlocProvider as a StatefulWidget, so that I am able to release the resources, when this widget is disposed.

Flutter is not able to instantiate a generic type <T>

As unfortunately Flutter is not able to instantiate a generic type, we have to pass the instance of the BLoC to the BlocProvider. To enforce the implementation of the dispose() method in each BLoC, all BLoCs have to implement the BlocBase interface.

Reminder on InheritedWidget

When we are using an InheritedWidget and are invoking the context.inheritFromWidgetOfExactType(...) method to get the nearest widget of the given type, this method invocation automatically registers this "context" (= BuildContext) to the list of ones which will be rebuilt each time a change applies to the InheritedWidget subclass or to one of its ancestors.

Please note that, in order to be fully correct, the problem related to the InheritedWidget as I have just explained only occurs when we combine the InheritedWidget with a StatefulWidget. When you simply use an InheritedWidget without a State, the issue does not happen. But... I will come back on this remark in a next article.

The type of Widget (Stateful or Stateless) linked to the BuildContext, does not matter.


Personal note on BLoC

Third rule related to BLoC says: "rely on exclusive use of Streams for both input (Sink) and output (stream)".

My personal experience relativises this statement a little bit... Let me explain.

At first, the BLoC pattern was conceived to share the very same code across platforms (AngularDart, ...) and, in this perspective, that statement makes full sense.

However, if you only intend to develop a Flutter application, this is, based on my humble experience, a little bit overkill.

If we stick to the statement, no getter or setter are possible, only sinks and streams. The drawback is "all this is asynchronous".

Let's take 2 samples to illustrate the drawbacks:

  • you need to retrieve some data from the BLoC in order to use this data as input of a page that should display these parameters right away (for example, think of a parameters page), if we had to rely on Streams, this makes the build of the page asynchronous (which is complex). Sample code to make it work via Streams could be like the following one... Ugly isn't it.


  • at the BLoC level, you also need to convert a "fake" injection of some data to trigger the provision of the data you expect to receive via a stream. Sample code to make this work could be:


I don't know your opinion, but personally, if I don't have any constraints related to code porting/sharing, I find this too heavy and I would rather use regular getters/setters when needed and use the Streams/Sinks to keep the separation of responsibilities and broadcast the information where needed, which is awesome.


It is now time to see all this in practice...

As mentioned in the beginning of this article, I built a pseudo-application to show how to use all these notions. The full source code can be found on Github.

Please be indulgent since this code is far from being perfect and could be much better and/or better architectured but the only objective is simply to show you how all this works.

As the source code is documented a lot, I will only explain the main principles.

Source of the movie catalog

I am using the free TMDB API to fetch the list of all movies, as well as the posters, rating and descriptions.

In order to be able to run this sample application, you will need to register and obtain the API key (which is totally free), then put your API key in the file "/api/tmdb_api.dart", line #15.

Architecture of the application

This application uses:

  • 3 main BLoCs:

    • ApplicationBloc (on top of everything), responsible for delivering the list of all movie genres;
    • FavoriteBloc (just underneath), responsible for handling the notion of "Favorites";
    • MovieCatalogBloc (on top of the 2 main pages), responsible for delivering the list of movies, based on filters;
  • 6 pages:

    • HomePage: landing page that allows the navigation to the 3 sub-pages;
    • ListPage: page that lists the movies as a GridView, allows filtering, favorites selection, access to the Favorites and display of the Movie details in a sub-sequent page;
    • ListOnePage: similar to ListPage but the list of movies is displayed as a horizontal list and the details, underneath;
    • FavoritesPage: page that lists the favorites and allows the deselection of any favorites;
    • Filters: EndDrawer that allows the definition of filters: genres and min/max release dates. This page is called from ListPage or ListOnePage;
    • Details: page only invoked by ListPage to show the details of a movie but also to allow the selection/unselection of the movie as a favorite;
  • 1 sub BLoC:

    • FavoriteMovieBloc, linked to a MovieCardWidget or MovieDetailsWidget to handle the selection/unselection of a movie as a favorite
  • 5 main Widgets:

    • FavoriteButton: widget responsible for displaying the number of favorites, real-time, and redirecting to the FavoritesPage when pressed;
    • FavoriteWidget: widget responsible for displaying the details of one favorite movie and allow its unselection;
    • FiltersSummary: widget responsible for displaying the filters currently defined;
    • MovieCardWidget: widget responsible for displaying one single movie as a card, with the movie poster, rating and name, as well as a icon to indicate the selection of that particular movie as a favorite;
    • MovieDetailsWidget: widget responsible for displaying the details related to a particular movie and to allow its selection/unselection as a favorite.

Orchestration of the different BLoCs / Streams

The following diagram shows how the main 3 BLoCs are used:

  • on the left side of a BLoC, which components invoke the Sink
  • on the right side, which components listen to the stream

As an example, when the MovieDetailsWidget invokes the inAddFavorite Sink, 2 streams are triggered:

  • outTotalFavorites stream forces a rebuild of FavoriteButton, and
  • outFavorites stream
    • forces a rebuild of MovieDetailsWidget (the "favorite" icon)
    • forces a rebuild of_buildMovieCard (the "favorite" icon)
    • is used to build each MovieDetailsWidget


Observations

Most of the Widgets and Pages are StatelessWidgets, which means that:

  • the setState() which forces to rebuild is almost never used. Exceptions are:

    • in the ListOnePage when a user clicks a MovieCard, to refresh the MovieDetailsWidget. This could also have been driven by a stream...
    • in the FiltersPage to allow the user to change the filters before accepting them, via Sink.
  • the application does not use any InheritedWidget

  • the application is almost 100% BLoCs/Streams driven which means that most of the Widgets are independent of each other and of their location in the application

    A practical example is the FavoriteButton which displays the number of selected favorites in a badge. The application counts 3 instances of this FavoriteButton, each of them, displayed in 3 different pages.

Display of the list of movies (explanation of the trick to display an infinite list)

To display the list of movies that meet the filters criteria, we use either a GridView.builder (ListPage) or a ListView.builder (ListOnePage) as an infinite scroll list.

Movies are fetched using the TMDB API in pages of 20 movies at a time.

As a reminder, both GridView.builder and ListView.builder take as input an itemCount which, if provided, tells the number of items to be displayed. The itemBuilder is called with index varying from 0 up to itemCount - 1.

As you will see in the code, I arbitrary provide the GridView.builder with an additional 30 more. The rationale is that in this example, we are manipulating a presumed infinite number of items (which is not totally true but who cares for this example). This will force the GridView.builder to request "up to 30 additional" items to be displayed.

Also, both GridView.builder and ListView.builder only call the itemBuilder when they consider that a certain item (index) has to be rendered in the viewport.

This MovieCatalogBloc.outMoviesList returns a List<MovieCard>, which is iterated in order to build each Movie Card. The very first time, this List<MovieCard> is empty but thanks to the itemCount: ...+30, we fool the system which will ask to render 30 non-existing items via the _buildMovieCard(...).

As you will see in the code, this routine makes a weird call to the Sink:



// Notify the MovieCatalogBloc that we are rendering the MovieCard[index]
    movieBloc.inMovieIndex.add(index);

This call tells the MovieCatalogBloc that we want to render the MovieCard[index].

Then the _buildMovieCard(...) goes on validating that data related to the MovieCard[index] exists. If yes, the latter is rendered, otherwise a CircularProgressIndicator is displayed.

The call to MovieCatalogBloc.inMovieIndex.add(index) is listened by a StreamSubscription that translates the index to a certain pageIndex number (one page counts up to 20 movies). If the corresponding page has not yet been fetched from the TMDB API, a call to the API is made. Once the page has been fetched, the new list of all fetched movies is sent to the _moviesController. As that stream ( = movieBloc.outMoviesList) is listened by the GridView.builder, the latter requests to rebuild the corresponding MovieCard. As we now have the data, we may render it.


Images that describe the PublishSubject, BehaviorSubject and ReplaySubject were published by ReactiveX.

Some other interesting articles worth reading:


Conclusion

Very long article but there is still much more to say about it as it is more than obvious to me that this is the way to go forward with the development of a Flutter application. It provides so much flexibility.

Stay tuned for new articles, soon. 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