BLoC is a popular design/architectural pattern used in software development to design and develop applications.
Android and iOS developers highly popularized the MVC model. The pattern involves the Model holding the data type, the View displaying the data from the Model, and the Controller standing in-between to manipulate and control both.
Flutter brought a new design pattern upon its inception, a variation of this MVC called BLoC.
Widgets in Flutter use this stream for communication and sending data.
Stream is of great benefit to the developers because it decouples the business logic from the UI, so that the code is easier to maintain, read, and test.
This design pattern helps separate presentation from business logic, making your code fast, easy to test, and reusable.
There are two concepts of the BLoC pattern we need to understand:
Cubit
is a class that extends Blocbase
; this class is used to set state and trigger state changes.
A Cubit
represents a part of your application state; so, we can give a state to manage, and the Cubit
will expose methods to us that we will use to make changes to the state.
To create a Cubit
, extend the Cubit
class:
class Score extends Cubit<int> {
Score(): super(0);
}
The above class, Score
, extends the Cubit
class – we have an initial state of 1
. Initial states are set in Cubit
via the super
method call. Cubit
can manage the state of any data type. In our example, our state is of the Int
type, and the initial state is 0
.
Cubit
has a state
property; this property is the current value of our state in the Cubit
. So, we can create an instance of our Score
cubit and access the state from the state
property:
final _score = Score();
_score.state; // 0
Let’s add methods we can use to make changes to the state:
class Score extends Cubit<int> {
Score(): super(0);
void addScore(score) => emit(state + score);
}
Cubit
has the emit
method, used to output or emit a new state. This emitted state becomes the current state of the Cubit
. Let’s say the initial state is 9 and emit(6)
is called. The current state will now be 6, not 9.
So, the addScore
method takes a score that adds to the current score state as arg
. The current state and the passing score are added together, and the result of the addition is emitted as the new state via the emit()
method.
final _score = Score();
_score.state // 0
_score.addScore(3)
_score.state // 3
_score.addScore(4)
_score.state // 7
Whenever a state is being changed in a Cubit, an onChange
method is called before the state change is made. The onChange
method is called with the current state and the next state.
class Score extends Cubit<int> {
Score(): super(0);
void addScore(score) => emit(state + score);
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
}
Whenever the addScore
is called to change the state, the onChange
method will be called before the state is changed. So, we will see a log on our console.
Bloc
Bloc
is similar to Cubit
, but it uses events Cubit
, the state is held in the Bloc
, then to change or add new state, events are added to the Bloc
.
When an event is added to the Bloc
, the onEvent
method is called. After this onEvent
method is called, a transform events
method is called. This method can be used to manipulate the events in the Bloc
.
A method mapEventToState
is called next with the event. This method then yields the state. This yielded state becomes the new state of the Bloc
.
Let’s transform our above Score
cubit example to Bloc
:
enum ScoreEvent { addScore }
class Score extends Bloc<ScoreEvent, int> {
Score(): super(0);
void addScore(score) => emit(state + score);
@override
Stream<int> mapEventToState(ScoreEvent event) async* {
switch(event) {
case ScoreEvent.addScore:
yield state + 1;
break;
}
}
}
To use it, do:
final _score = Score();
_score.state; // 0
_score.add(ScoreEvent.addScore);
This will emit ScoreEvent.addScore
to the Bloc
. The mapEventToState
method will be called, and the state in the Score
bloc will be incremented to 1.
_score.state; // 1
BlocBuilder
The above sections showed us how to use streams in Dart. Now, we will see how to use them in Flutter.
In Dart, streams of data are created using the Stream
object, and we have the:
StreamController
to control what is emitted to the Stream and close it when done.StreamSubscription
to listen on a Stream.We also have a widget in Flutter, BlocBuilder
. This widget listens on a Stream and rebuilds whenever data is emitted into the stream.
The BlocBluider
passed a Bloc and a builder
function in its bloc
arg. This builder
function returns the widget tree of the BlocBuilder
. The BlocBuilder
listens on the Bloc and rebuilds the widget tree in its builder
function when the Bloc
changes.
class ScorePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Score')),
body: BlocBuilder<Score, int>(
bloc: Score,
builder: (context, state) {
return Center( child: Text("Score: $state"))
}
);
);
}
}
In the above code, the BlocBuilder
widget takes a bloc
with the Score
bloc as its value. Then, the builder
function renders a Text
widget.
Next, the BlocBuilder
will get the state from the Score
bloc and call the builder
function to render the widget it returns. The BlocBuilder
will then pass the current widget, BuildContext
, and the current state of the Score
bloc to the builder
function.
This will make the widgets in the returned tree able to access the state from the Score
bloc. The Text
is able to get the score value from the state
value and render it.
When the state
from the Score
bloc changes, BlocBuilder
will re-render or re-build the widget tree. This means that BlocBuilder
will get the new state from the Score
bloc. It will also call its builder
function with the current BuildContext
instance and the new state in the state
. So, the Text
widget will display the current score.
BlocBuilder
Remember when we used the add
method to add an event to a Bloc in a Bloc? Well, BlocBuilder
adds the add
method to the context
.
So, from the context
, we call the add
method and pass in the event we want to emit. This will change the Bloc state, and BlocBuilder
will re-build its tree.
class ScorePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Score')),
body: BlocBuilder<Score, int>(
bloc: Score,
builder: (context, state) {
return Center( child: Column( children: [Text("Score: $state"), FlatButton( child: Text("Add Score"), onPressed: () {
context.read<Score>().add(ScoreEvent.addScore),
})]))
}
);
);
}
}
The context.read<Score>().add(ScoreEvent.addScore)
call will change the state in the Score
bloc. Then, the BlocBuilder
will re-build the widget tree so that the Text
widget will display the new value of the state.
Here, we demonstrated the awesome power of the BLoC pattern and how it can be used in a Flutter app.
The BLoC pattern is reactive and decouples the UI logic from the business logic for it to be developed independently. The BLoC pattern makes our code easier maintain, scale, and test.