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
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.
CounterBloc will hold and notify a state of type
int and will manage an
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:
Button has dispatched the event it won’t be necessary for the UI to know what happens next. It’s BLoC business. Moreover,
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
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
RegionalReportLoading ), but some others will convey data, in particular:
RegionalReportLoadedwill deliver the loaded reports
RegionalReportLoadingErrorwill deliver a reason for the error
Now we can go for the proper BLoC implementation.
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
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
Let’s break this thing down:
- line 8–9: we’re saying that
ClosableBloc(which is a
Blocthat maps instances of
RegionalReportBlocEventto instances of
- line 10 to 13: we just define the dependency from
InfectionReportServicewhich 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 usual
async: this is because the return type is a
Stream. Then we just switch among the possible types of event and act accordingly,
yielding 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:
RouteHandlergets invoked by the
onGenerateRouteto create the route (I’m using named routes in the project and this package)
- I’m using
RegionalReportBloc. For one bloc only, just use
BlocProviderand specify a
- I’m using
DependencyProviderto 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
mapEventToStatemaking them yield the loading state, call the service, the API, and then yield the output state to whoever is listening.
- Once again: remember the
InheritedWidgetstuff above? Note how since both
InfectionsMapuse data from those blocs no injection is needed at all!
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
Line 15 uses a
BlocBuilder which will listen to the state of a
RegionalReportBloc and manage instances of
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
BlocConsumerin the exact same fashion we did before with
- line 24 tells to trigger the listener function only if the current state is
- line 25 to 35 is the actual function that brings up the
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!