Hey there 😊

If you like what you read, feel free to …

🥗Buy me a salad
Share this 💬

Applying the BLoC pattern in practice

Applying the BLoC pattern in practice
This article has been updated! Tap to see
  • April 1, 2022: Updated the code in the article samples to use null safety

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:

1dependencies:
2flutter:
3sdk: flutter
4flutter_bloc: ^7.3.2
5equatable: ^2.0.3

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:

Flutter calculator keypad complete
Calculator with keyboard

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)
Flutter BLoC pattern event overview
Overview of events and their triggers

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

Comments (5) ✍️

marcin

CalculationModel copyWith({ int firstOperand, String operator, int secondOperand, int result, }) { return CalculationModel( firstOperand: firstOperand ?? this.firstOperand, operator: operator ?? this.operator, secondOperand: secondOperand ?? this.secondOperand, result: result ?? this.result, ); }

Passing null as the argument still will not change anything. I dont understand why you use functions instead.

Reply to marcin

Matt

This tutorial probably needs updated with sound null safety just like the Calculator App tutorial. Do you have plans on updating them?
Reply to Matt

Marc
In reply to Matt's comment

Hey Matt,

you have a good point there. I’ll put it on my list! :)

Reply to Marc

Marc
In reply to Matt's comment

Hey Matt,

it took me some time, for sure. but eventually I did it 🙂. It’s been updated with sound null safety!

Reply to Marc

Krishna

I would say this is how a bloc should be explained.

There are many which dont even explain some of the practical things.

Thnks for the tutorial.

Reply to Krishna

Comment this 🤌

You are replying to 's commentRemove reference