BLoC State Management in Flutter
When you start developing an application, one of the first things to ask is: how can I design the project so it is testable, modular, and flexible? In our last post on starting a Flutter project (only available in French), we talked about the different patterns in a Flutter environment.
Today, we will look at using Business Logic Components (BLoC), one of the best-known Flutter state management patterns.
If you have ever created an application in Flutter before, you may be familiar with the Counter App. This demo app comes with Flutter. It provides a basic template and highlights some of Flutter’s benefits, such as HotReload or HotRestart.
This demo app is pretty simple: it takes a page with a button and text indicating the number of times the button is pressed (hence the name “Counter App”).
In this post, we’re going to change the Counter App demo app step by step using the BLoC design pattern and its components.
BLoC Design Pattern
The architecture of an application is often one of the most hotly debated topics in application development. Everyone seems to have their preferred design model.
Mobile app developers separate the project over multiple layers to distribute the responsibilities better between the modules. The view and model are separate, with the controller (or ViewModel) sending events between them. These approaches, well known to developers, are Model-View-Controller (MVC) or Model-View-ViewModel (MVVM). A variant of this classic model has emerged from the Flutter community: BLoC.
BLoC stands for Business Logic Components. The main thing to remember with BLoC is that everything in the app should be represented as a stream of events. The widget reproduces events: interaction with the graphical user interface, system event, orientation change, etc. The BLoC is positioned after the widget and listens and responds to the events produced by the widget. In a way, the purpose of BLoC is to manage the life cycle of a widget. The Dart programming language even includes a built-in syntax for working with streams. So, BLoC can be considered a critical architecture when using Flutter to control a widget containing business logic.
Click the image to start the animation.
Let’s add a home_bloc.dart file to the project. This file represents our BLoC and will contain the counter value and increase logic.
Interaction with the widget is done through the StreamController: this is a Dart framework object for controlling streams. It is used to transmit data, events, and all types of objects from one location to another.
Let’s add two stream controllers in HomeBloc:
- one to send the counter increase event from the widget to the BLoC
- the other to send the current value from the counter to the widget
Think of the StreamController as a pipe. Every pipe contains two sections: one is connected to the data source and the other to the consumer.
The side connected to the source in Flutter is called Sink. The one connected to the consumer is called Stream.
Click the image to start the animation.
We have two StreamControllers in HomeBloc:
- _counterInStreamController: to send the counter value to the view
- _counterOutStreamController: to send the increment event to the counter
I subscribe to the _counterInStreamController stream in the HomeBloc constructor to manage the view events.
If IncreaseEvent occurs in the stream handler, the counter needs to be increased and returned to the view by calling _counterOutStreamController.sink.add.
My HomeBloc shows counterInSink and counterOutStream so the view can interact with the BLoC.
Our HomeBloc is ready to be consumed by the widget. We could create the BLoC instance in the widget directly, but instead of doing that, let’s talk about Inversion Control (IoC) in Flutter.
IoC in Flutter
Unlike the .Net ecosystem, reflections are prohibited by design in Flutter due to the potential detriment to application performance. However, that doesn’t mean that IoC and dependency injection are impossible in Flutter.
The default way of providing objects/services to the widgets in this framework is through InheritedWidgets. If you want a widget or its template to access a service, the widget must be a child of the inherited widget, resulting in unnecessary nesting.
This is where dependency injection comes in. An IoC lets you register your class types and request it from anywhere as long as you have access to the container. Get_it, the package maintained by the Flutter community, ensures a certain level of stability here.
To add the package to our project, we need to add the following line in pubspec.yaml:
The next step is to configure the container with the dependencies we will be using in the project. Before creating the application, I configure the IoC in the Main function.
The container instance is static, and, to access it, you need to call GetIt.instance from where it is needed. Here, I register my HomeBloc as a factory in registerServices: the container will then create a BLoC instance on demand.
In the example below, I register my injections in the main method just before running the application:
Now we have configured the container and the BLoC, let’s move on to the widget creation and consumption step.
Stateful Widget and BLoC
Everything is a widget in Flutter. Each widget is used to describe what its view should look like based on its configuration and state. When a widget’s state changes, the framework calculates the difference from the previous state to determine the minimal changes needed in the underlying render tree to go from one to the other.
Flutter has two widget types:
- The Stateless Widget is useful when the part of the user interface you are describing does not depend on anything other than the object configuration information and the BuildContext in which the widget is initialized.
- The Stateful Widget: unlike the Stateless Widget, the Stateful Widget contains the instance of the component state. The Stateful Widget consists of two parts: the immutable Stateful Widget is the instance of its state.
In our case, these are the HomePage and HomePageState classes. The link between the widget and its state is created in the createState() method that initializes the state instance. This is all the Stateful Widget should contain in most cases. However, I have a personal preference: I like to have the view code and its codebehind in two different objects. To make that possible, I created a buildWidget method that takes care of the visual rendering of the widget and is called when building the widget.
The widget contains three elements: a static text, text representing the counter, and the FloatingActionButton to increase the counter. You can see that the counter TextWidget is surrounded by the StreamBuilder that consumes the stream when there is a data change.
To retrieve the counter, use snapShot.data.toString() and the TextWidget displays the counter value sent by HomeBloc. The FloatingActionButton event handler is located within the state object directly.
MyHomePageState represents the widget codebehind. It contains the BLoC instance, life cycle events, and the widget event handler.
The BLoC instance is retrieved in the didChangeDependencies method, which is only called once when the widget is created in the memory. The other methods can be called several times during the widget’s lifetime.
The build method is called whenever the framework decides to redraw the widget.
And, of course, a method has BLoC to release the instance.
So, there you have it! You have modified the demo Counter App to use BLoC. Now you know how to use this pattern in your own projects.
Let us know what your favorite state management patterns are in the comments!