Using the BLoC pattern for clean Flutter apps, theory and a practical example
While it can be an overlooked thing in a small project (which I don’t think it should either), when it comes to a medium/large project it’s necessary to take in account the fact that we need a clean, modular, testable and robust architecture for our app. If it wasn’t so, a lot of headaches will come on the long run, when trying to figure out some bug, implementing new features, changing existing ones and performing a lot of more “mutations” our app might go into.
The goal of a design pattern is to provide a clean standard for how our project will have its files organised, how the components will interact with each other, separating layers so that a change into one is transparent to the others, and, mostly, promoting the reuse of blocks of code.
There are a lot of design patterns that people use for Flutter. Some of the names you might’ve stepped on are Redux, Provider, InheritedWidget, MobX, MVC, MVP. They are all different (sometimes not so different) ways to manage the state of an app, deciding how it mutates according to the interaction with the user and/or with other environment’s agents.
Quick detour: the importance of lifting the state up
Suppose you have a simple application, suppose that’s the counter app that you get when creating a Flutter project (assuming you’ve not fiddled with the default template).
Keeping that idea in mind and complicating it a bit, that app can become something like this:
Then, suppose you want to show the counter in a different way, for example you want the text coloured in red (and bold, italic, underlined, upside down, you name it). What do you do?
You might think to create a FancyCounterTextWidget
, pass the value and voilà.
You’re going to have something like this:
Yay. Well, what happens if your CounterText
and FancyCounterText
widgets need to be moved somewhere else, let’s say in another widget? You’re going to need to inject the counter
passing it to the constructor, and so on. And what if you need to move/copy that widget to another part of the app? This will result in an injection nightmare!
This is why Flutter provides us with InheritedWidget
which allows us to lift these dependencies above the widget subtree and provides access to those dependencies to every widget below the subtree. This is what the Provider
package does in a fancy way.
This concept is what we need to explain what a BlocProvider
is later in this article. We’re not going to dig about how these are implemented since it’s out of the scope of this article.
The BLoC pattern
So what is this? BLoC stands for Business Logic Component, and a BLoC is essentially a class which keeps the state of our app/feature/screen/widget, mutates it upon receiving input events and notifying its change.
Using a BLoC to manage the state of the previous screen, our widget tree will look like this:
The dashed lines indicate that while those widgets are not direct children of the BlocProvider
they are its descendants and have access to the CounterBloc
it provides.
The CounterBloc
will hold and notify a state of type int
and will manage an IncrementCounterEvent
.
So now when the Button
is tapped, an IncrementCounterEvent
will be dispatched to the CounterBloc
who will increment its counter and notify the state change to whoever is listening:
This approach has many benefits:
Once the Button
has dispatched the event it won’t be necessary for the UI to know what happens next. It’s BLoC business. Moreover, CounterText
and FancyCounterText
do not care if the increment came from the press of a button or an accelerometer spike or a solar eclipse, and this means we can easily mock states in order to test those components.
That’s it. This is the BLoC pattern core logic in a nutshell. Now that we’ve introduced it, let’s see an example that is surely more practical and realistic than a dummy counter app.
A practical example: Corona Italy
I’ve been a BLoC user for quite some time now, but before writing this article I wanted to try implementing it with the flutter_bloc package. Using it avoids some boilerplate and helps you implementing the BLoC pattern without necessarily knowing all of its ins and outs right away.
The project is open and you can find it on GitHub. It’s a simple front end (with a very poor graphic for now) to display the daily Covid-19 infections in Italy using the government open data.
It consists of:
- a home screen which has a map and a bottom sheet containing the national highlights and the list of regions
- a screen which shows the full report for the national data
- a screen which shows the full report for a region and a list of its provinces along the increment of cases for each one of them
So, we have 3 types of information:
- National report
- Regional report
- Provincial report
Which means, three BLoCs.
Since this is a simple app, the core logic of these BLoCs is the same. They differ only in the type of event, the api request and response mapping. We’re going to take a closer look to the BLoC that provides the regional reports.
Events and states
First thing first, we’re going to define the events and the states which the BLoC is going to work and communicate with. Mind that an event is the “input action” and a state is one of the possible outputs.
Don’t mind the base classes InfectionReportBlocEvent
and InfectionReportBlocState
, they are empty and exist only to group conceptually the various events and types under the same semantic space.
So we have the event:
In this case we define only the fetch event. We could have defined other events in case we wanted to do different things, but since this is a read-only api only fetch makes sense here.
And then we define our possible states:
Some states do not need any extras (like RegionalReportIdle
and RegionalReportLoading
), but some others will convey data, in particular:
RegionalReportLoaded
will deliver the loaded reportsRegionalReportLoadingError
will deliver a reason for the error
Now we can go for the proper BLoC implementation.
The Bloc
Since in my app I needed to know if a bloc was disposed (for dependency injection purposes), I’ve defined a ClosableBloc
class which extends the real Bloc
class provided by flutter_bloc
:
This simply overrides the close
method of the base class Bloc
(which is another way to say, conceptually, dispose), updates a closed
variable and calls super.
That said, defining a ClosableBloc
implementation is exactly the same thing than using che original Bloc
class.
Let’s break this thing down:
- line 8–9: we’re saying that
RegionalReportBloc
is aClosableBloc
(which is aBloc
that maps instances ofRegionalReportBlocEvent
to instances ofRegionalReportState
- line 10 to 13: we just define the dependency from
InfectionReportService
which is the layer that will query the API. This is out of the scope of the article but still provides a good example for an asynchronous operation and a more realistic scenario than an increment button. - line 14 to 31: the mapping function. This is the heart of the implementation. First thing you’ll notice is the
async*
which is not the usualasync
: this is because the return type is aStream
. Then we just switch among the possible types of event and act accordingly,yield
ing the corresponding state. - line 33 to 37: the function that fetches and maps the data, nothing fancy. It’s out of the scope of this article, but feel free to dig into the repository’s code if you want to know more :)
So far we have defined the first bloc. As for the others, the structure of events, states, and mapping is exactly the same.
Let’s hook up the UI
First things first: we need to provide the bloc to our screen. Remember the whole InheritedWidget
thing discussed before? We’re going to do exactly that: make the bloc available to each widget of the screen (the subtree).
This is the Home route handler. For the purpose of this article you just need to know this:
- This
RouteHandler
gets invoked by theonGenerateRoute
to create the route (I’m using named routes in the project and this package) - I’m using
MultiBlocProvider
becauseHomeScreen
needs bothNationalReportBloc
andRegionalReportBloc
. For one bloc only, just useBlocProvider
and specify achild
for it. - I’m using
DependencyProvider
to get the instances of the blocs for dependency injection reasons, instantiating the blocs directly by their constructor is exactly the same thing.
At this point we have the home route which returns a HomeScreen
wrapped by a provider for the blocs it needs. Let’s see how the widgets hook up to those blocs. We’re almost done, hang in there!
Here’s the code home screen:
Points of interest of the code:
- line 21 to 24: after the first frame is rendered, send the fetch event for the blocs. This will trigger their implementation of
mapEventToState
making them yield the loading state, call the service, the API, and then yield the output state to whoever is listening. - Once again: remember the
InheritedWidget
stuff above? Note how since bothHomePanel
andInfectionsMap
use data from those blocs no injection is needed at all!
Just to clarify: the tr
in the Text
is something used by easy_localization for multi language and SlidingUpPanel
is a widget provided by this package.
Let’s start with taking a look at the HomePanel
widget, which will lead us to see the simplest case of hooking up a bloc:
The widget is made of two widgets. The first one, NationalReportWidget
is the one that hooks up to the NationalReportBloc
, which we won’t see here, and the second one is RegionsReportList
that hooks up to the bloc we’ve seen above:
As you might have guessed, the key to everything here is BlocBuilder
.
Line 15 uses a BlocBuilder
which will listen to the state of a RegionalReportBloc
and manage instances of RegionalReportState
.
Then the builder allows us to specify what we want to build upon receiving a state update. For the sake of brevity I’ve omitted the _Body
widget code since it’s just normal UI stuff.
I’d say this is it. Of course BlocBuilder
allows specifying other stuff like directly providing an instance of the bloc, or specifying condition for rebuilding, and so on. But since this is more an article on bloc than on flutter_bloc capabilities I won’t dig in there too much and leave you the docs at the end of the article.
That said, even this might be a good place to stop, I think it’s useful to take a quick look at the InfectionsMap
implementation, which features something I really like from the package and the bloc pattern itself.
We’ve seen that we can build an error widget when receiving an error state from the bloc, but what if we need to act differently? This is the case for InfectionsMap
: the map is shown, then the data is loaded and the map is updated with the markers. But what if the loading doesn’t fall through? Drawing a “there was an error” error widget on the map doesn’t sound like a good idea, so what? Informing the user via a SnackBar
seems a pretty good way to me (a dev without much of design skills).
In order to do this we should conceptually have a BlocBuilder
which reacts to the changes of state, and another listener which brings the SnackBar
up. Luckily for us, the flutter_bloc package gives us a BlocConsumer
widget, which allows to do both of the things in one place!
- at line 23 we use
BlocConsumer
in the exact same fashion we did before withBlocBuilder
- line 24 tells to trigger the listener function only if the current state is
RegionalReportLoadingError
- line 25 to 35 is the actual function that brings up the
SnackBar
That’s it. As you can see we have two widgets listening to the same RegionalReportBloc
(the map and the report list) which do very different things with the very same source of truth, and those things are not necessarily building widgets but calling other things too.
This is one of the best things about BLoC, if you ask me!
It’s been a long road and we’ve seen quite a lot. My suggestion is not to be afraid of all of this but just let this sink in a bit(pun intended) and get the hang of it. I’ve used a more rudimental implementation of the BLoC pattern in a banking app I’ve developed as a consultant in a team of two devs and I can assure you that even if it was less modular than this approach the pattern really paid off on the long run in terms of maintainability, mockability, debugging and features extension and change (it was pretty much frequent for the client’s specs to change while still developing and testing).
Before I say bye, I’ll leave here some links:
- GitHub repository for the complete project: https://github.com/magicleon94/corona_italy/tree/medium-article
- Bloc documentation: https://bloclibrary.dev/
- flutter_bloc readme and docs: https://pub.dev/packages/flutter_bloc
That’s all folks! Hope you’ve enjoyed this and found it more useful than boring!
Cheers!
Antonello