Using the BLoC pattern for clean Flutter apps, theory and a practical example

Antonello Galipò
Sep 6, 2020 · 10 min read

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

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

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

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

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 reports
  • RegionalReportLoadingError will deliver a reason for the error

Now we can go for the proper BLoC implementation.

The 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 a ClosableBloc (which is a Bloc that maps instances of RegionalReportBlocEvent to instances of RegionalReportState
  • 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 usual async : this is because the return type is a Stream . 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

This is the Home route handler. For the purpose of this article you just need to know this:

  • This RouteHandler gets invoked by the onGenerateRoute to create the route (I’m using named routes in the project and this package)
  • I’m using MultiBlocProvider because HomeScreen needs both NationalReportBloc and RegionalReportBloc . For one bloc only, just use BlocProvider and specify a child 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 both HomePanel and InfectionsMap 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 with BlocBuilder
  • 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:

That’s all folks! Hope you’ve enjoyed this and found it more useful than boring!



Flutter Community

Articles and Stories from the Flutter Community