There are numerous documentations on the web explaining the purpose of the BLoC pattern and how it’s applied to minimal examples. This tutorial aims for the migration of an existing app to this state management architecture.
For that, we use the calculator app whose creation was explained in another tutorial. The whole app’s state is managed by a StatefulWidget. Let’s tweak it to let the entire state be managed by a BLoC which will lead to a separation of the UI from the business logic.
Motivation
Why do we want such a migration in the first place? As long as everything works, we can keep it that way, can’t we?
Yes and no. Of course the current implementation works. However, if we were to change anything, we were unable to say with certainty that the functionality would not break.
This problem is nothing new. That’s why people invented software testing. But now we come to the core question: is this implementation even testable? What would our unit-tests look like if we wanted to ensure the current implementation?
The answer is: it’s not. Our widget has two responsibilities: displaying the UI and defining the business logic. Changing parts of the UI could possibly break the business logic because it’s not strictly separated. Same applies the other way around.
Uncle Bob‘s opinion on that: a responsibility is defined as a reason to change. Every class or module should have exactly one reason to be changed.
Implementation
Implementing the BLoC pattern involves a lot of boilerplate code (Bloc, Event, State and all of its abstract logic). That’s why we make it easy for us and use a prefabricated solution: the bloc library. The package contains several classes that prevents us from implementing the pattern ourselves.
Let’s add the dependency to the project:
Events
Let’s start with the events. That’s quite easy because a glimpse at the UI can pretty quickly make us realize which events we need:
There are four types of buttons the user can press resulting in four distinct interactions:
- Pressing a number button (0-9)
- Pressing an operator button (+, -, x, /)
- Pressing the result calculation button (=)
- Pressing the “clear” button (c)
Let’s translate these formal requirements into events in the BLoC context:
1import 'package:equatable/equatable.dart';
2import 'package:meta/meta.dart';
3
4abstract class CalculationEvent extends Equatable {
5 const CalculationEvent();
6}
7
8class NumberPressed extends CalculationEvent {
9 final int number;
10
11 const NumberPressed({required this.number});
12
13 @override
14 List<Object> get props => [number];
15}
16
17class OperatorPressed extends CalculationEvent {
18 final String operator;
19
20 const OperatorPressed({required this.operator});
21
22 @override
23 List<Object> get props => [operator];
24}
25
26class CalculateResult extends CalculationEvent {
27 @override
28 List<Object> get props => [];
29}
30
31class ClearCalculation extends CalculationEvent {
32 @override
33 List<Object> get props => [];
34}
The only events that require a parameter are NumberPressed
and OperatorPressed
(because we need to distinguish which one was taken). The two other events don’t have any properties.
States
The states are fairly easy. That’s because our UI does not care about what exactly happened. It only cares about the situation when the whole calculation (consisting of two operands, an operator and a result) changes. That’s why we only need one actual state saying that the calculation has changed. Additionally we need one initial state. Since we don’t have any asynchronous operations, we don’t need a “loading” state either.
1import 'package:equatable/equatable.dart';
2import 'package:meta/meta.dart';
3
4import '../calculation_model.dart';
5
6abstract class CalculationState extends Equatable {
7 final CalculationModel calculationModel;
8
9 const CalculationState({required this.calculationModel});
10
11 @override
12 List<Object> get props => [calculationModel];
13}
14
15class CalculationInitial extends CalculationState {
16 CalculationInitial() : super(calculationModel: CalculationModel());
17}
18
19class CalculationChanged extends CalculationState {
20 final CalculationModel calculationModel;
21
22 const CalculationChanged({required this.calculationModel})
23 : super(calculationModel: calculationModel);
24
25 @override
26 List<Object> get props => [calculationModel];
27}
Instead of duplicating every property in both of the states, we use a separate class called CalculationModel
. This way, if we change something (e.g. displaying the last result as well), we only need to change one model. This is what the model looks like:
1import 'package:equatable/equatable.dart';
2
3class CalculationModel extends Equatable {
4 CalculationModel({
5 this.firstOperand,
6 this.operator,
7 this.secondOperand,
8 this.result,
9 });
10
11 final int? firstOperand;
12 final String? operator;
13 final int? secondOperand;
14 final int? result;
15
16 @override
17 String toString() {
18 return "$firstOperand$operator$secondOperand=$result";
19 }
20
21 @override
22 List<Object?> get props => [firstOperand, operator, secondOperand, result];
23}
It’s important to let this model extend Equatable
. Same goes for the states. This is because we need the UI to change only when something has actually changed. We need to define how Dart decides whether there was a change. This is done by defining props
.
BLoC
1class CalculationBloc extends Bloc<CalculationEvent, CalculationState> {
2 CalculationBloc() : super(CalculationInitial());
3
4 @override
5 Stream<CalculationState> mapEventToState(
6 CalculationEvent event,
7 ) async* {
8 if (event is ClearCalculation) {
9 yield CalculationInitial();
10 }
11 }
12}
There is only one method we need to override: mapEventToState()
. This method is responsible for handling incoming events and emitting the new state. We start with the simplest one: ClearCalculation
. This makes it emit the initial state again (CalculationInitial
).
Next one: OperatorPressed
event.
1Future<CalculationState> _mapOperatorPressedToState(
2 OperatorPressed event,
3) async {
4 List<String> allowedOperators = ['+', '-', '*', '/'];
5
6 if (!allowedOperators.contains(event.operator)) {
7 return state;
8 }
9
10 CalculationModel model = state.calculationModel;
11
12 return CalculationChanged(
13 calculationModel: CalculationModel(
14 firstOperand: model.firstOperand == null ? 0 : model.firstOperand,
15 operator: event.operator,
16 secondOperand: model.secondOperand,
17 result: model.result
18 )
19 );
20}
We notice something: it’s crucial that we use a new instance of the model. If we work with the reference, the bloc library will have problems checking the equality and thus might fail to let the UI know about the update. However, we need to let the new instance have the old values so that we don’t lose state when we emit a new CalculationState
.
This seems like a lot of boilerplate code if we have a method that handles several cases with several states as outcome. That’s why we enhance the CalculationModel
by a method called copyWith()
:
1CalculationModel copyWith({
2 int Function() firstOperand,
3 String Function() operator,
4 int Function() secondOperand,
5 int Function() result
6}) {
7 return CalculationModel(
8 firstOperand: firstOperand?.call() ?? this.firstOperand,
9 operator: operator?.call() ?? this.operator,
10 secondOperand: secondOperand?.call() ?? this.secondOperand,
11 result: result?.call() ?? this.result,
12 );
13}
You might ask yourself why we expect functions and not the date types itself. It’s because otherwise null values are treated exactly as not given parameters. I will explain in another article in more detail.
Now we can write the very same method _mapOperatorPressedToState
like this:
1Future<CalculationState> _mapOperatorPressedToState(
2 OperatorPressed event,
3) async {
4 List<String> allowedOperators = ['+', '-', '*', '/'];
5
6 if (!allowedOperators.contains(event.operator)) {
7 return state;
8 }
9
10 CalculationModel model = state.calculationModel;
11
12 CalculationModel newModel = state.calculationModel.copyWith(
13 firstOperand: () => model.firstOperand == null ? 0 : model.firstOperand,
14 operator: () => event.operator
15 );
16
17 return CalculationChanged(calculationModel: newModel);
18}
Let’s continue with the handling of the CalculateResult
event:
1Future<CalculationState> _mapCalculateResultToState(
2 CalculateResult event,
3) async {
4 CalculationModel model = state.calculationModel;
5
6 if (model.operator == null || model.secondOperand == null) {
7 return state;
8 }
9
10 int result = 0;
11
12 switch (model.operator) {
13 case '+':
14 result = model.firstOperand + model.secondOperand;
15 break;
16 case '-':
17 result = model.firstOperand - model.secondOperand;
18 break;
19 case '*':
20 result = model.firstOperand * model.secondOperand;
21 break;
22 case '/':
23 if (model.secondOperand == 0) {
24 CalculationModel resultModel = CalculationInitial().calculationModel.copyWith(
25 firstOperand: () => 0
26 );
27
28 return CalculationChanged(calculationModel: resultModel);
29 }
30 result = model.firstOperand ~/ model.secondOperand;
31 break;
32 }
We just apply the logic, we had before the transformation. Notable here is that a division by zero results in “0” as the first operand for the next calculation.
Now, the biggest event is NumberPressed
because depending on the state of calculation we’re currently in, pressing a number can have different effects. That’s why the handling is a little bit more complex here:
1Future<CalculationState> _mapNumberPressedToState(
2 NumberPressed event,
3 ) async {
4 CalculationModel model = state.calculationModel;
5
6 if (model.result != null) {
7 CalculationModel newModel = model.copyWith(
8 firstOperand: () => event.number,
9 result: () => null
10 );
11
12 return CalculationChanged(calculationModel: newModel);
13 }
14
15 if (model.firstOperand == null) {
16 CalculationModel newModel = model.copyWith(
17 firstOperand: () => event.number
18 );
19
20 return CalculationChanged(calculationModel: newModel);
21 }
22
23 if (model.operator == null) {
24 CalculationModel newModel = model.copyWith(
25 firstOperand: () => int.parse('${model.firstOperand}${event.number}')
26 );
27
28 return CalculationChanged(calculationModel: newModel);
29 }
30
31 if (model.secondOperand == null) {
32 CalculationModel newModel = model.copyWith(
33 secondOperand: () => event.number
34 );
35
36 return CalculationChanged(calculationModel: newModel);
37 }
38
39 return CalculationChanged(
40 calculationModel: model.copyWith(
41 secondOperand: () => int.parse('${model.secondOperand}${event.number}')
42 )
43 );
44}
But again, we’re just copying off the former behavior from the version of the app without BLoC pattern.
Last thing to do is letting the widget emit the events and react on the emitted states of the BLoC.
1@override
2Widget build(BuildContext context) {
3 return MaterialApp(
4 debugShowCheckedModeBanner: false,
5 title: 'Flutter basic calculator',
6 home: Scaffold(
7 body: BlocProvider(
8 create: (context) {
9 return CalculationBloc();
10 },
11 child: Calculation(),
12 ),
13 ),
14 );
15}
First off, in the main.dart
, we need to add a BlocProvider
. This is necessary to enable our widget to communicate with the BLoC.
Now, instead of calling setState
whenever we recognize an interaction with the UI, we emit an event:
1numberPressed(int number) {
2 context.bloc<CalculationBloc>().add(NumberPressed(number: number));
3}
4
5operatorPressed(String operator) {
6 context.bloc<CalculationBloc>().add(OperatorPressed(operator: operator));
7}
8
9calculateResult() {
10 context.bloc<CalculationBloc>().add(CalculateResult());
11}
12
13clear() {
14 context.bloc<CalculationBloc>().add(ClearCalculation());
15}
Now, if we want the UI to display the result accordingly, we need to wrap the ResultDisplay
with a BlocBuilder
this gives us the ability to react on an emitted state.
1BlocBuilder<CalculationBloc, CalculationState>(
2 builder: (context, CalculationState state) {
3 return ResultDisplay(
4 text: _getDisplayText(state.calculationModel),
5 );
6 },
7),
8...
9String _getDisplayText(CalculationModel model) {
10 if (model.result != null) {
11 return '${model.result}';
12 }
13
14 if (model.secondOperand != null) {
15 return '${model.firstOperand}${model.operator}${model.secondOperand}';
16 }
17
18 if (model.operator != null) {
19 return '${model.firstOperand}${model.operator}';
20 }
21
22 if (model.firstOperand != null) {
23 return '${model.firstOperand}';
24 }
25
26 return "${model.result ?? 0}";
27}
Final words
The BLoC pattern is fantastic way of separating UI concerns from business logic concerns. Also, it enables the developer to manage state leaving the widget only as a reactive presentation layer. It might be a little bit confusing at first, but once you get the fundamental thoughts, it is not so hard to apply this pattern. I hope this little example cleared it up a little bit.
If you want the full source, there is the repository before (current master) and after the BLoC PR:
GET FULL CODE
marcin
Matt
Marc
In reply to Matt's comment
Marc
In reply to Matt's comment
Krishna