Flutter -BLoC Pattern

Santhosh Adiga U
5 min readMar 3, 2023

--

The BLoC (Business Logic Component) pattern is a popular design pattern used for managing state in Flutter applications. The BLoC pattern separates the UI from the business logic, making it easier to maintain and test your application. In this article, we will explore how to use the BLoC pattern in Flutter and how to test it with code and examples.

Getting Started with BLoC Pattern

The BLoC pattern consists of three main components: the UI, the BLoC, and the repository. The UI is responsible for rendering the view and sending events to the BLoC. The BLoC is responsible for processing events and emitting new states. Finally, the repository is responsible for fetching and storing data.

To get started with the BLoC pattern in Flutter, we need to create a new project and install the bloc package. We can create a new project by running the following command in the terminal:

flutter create my_app

Next, we need to add the bloc package to our project by adding the following line to the dependencies section of the pubspec.yaml file:

dependencies:
flutter:
sdk: flutter
bloc: ^8.0.0

Now we are ready to start building our app using the BLoC pattern.

Building a Simple Counter App using BLoC

Let’s start by building a simple counter app using the BLoC pattern. Our app will have two buttons: one to increment the counter and one to decrement the counter. The current value of the counter will be displayed on the screen.

Step 1: Create the CounterEvent

The first step is to create an event that will be sent to the BLoC when the user interacts with the UI. We will create a CounterEvent class with two sub-classes: IncrementEvent and DecrementEvent.

abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}

class DecrementEvent extends CounterEvent {}

Step 2: Create the CounterState

The second step is to create a state that will be emitted by the BLoC when it receives an event. We will create a CounterState class with a single property: value.

class CounterState {
final int value;

CounterState({required this.value});
}

Step 3: Create the CounterBloc

The third step is to create the BLoC that will process the events and emit new states. We will create a CounterBlocclass that extends the Bloc class from the bloc package.

class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(value: 0));

@override
Stream<CounterState> mapEventToState(CounterEvent event) async* {
if (event is IncrementEvent) {
yield CounterState(value: state.value + 1);
} else if (event is DecrementEvent) {
yield CounterState(value: state.value - 1);
}
}
}

The CounterBloc class extends the Bloc class and takes two generic parameters: CounterEvent and CounterState. We initialize the state with a value of 0 in the constructor. The mapEventToState method is called whenever a new event is sent to the BLoC. We check the type of the event and emit a new state with the updated counter value.

Step 4: Create the CounterPage

The final step is to create the UI that will interact with the BLoC. We will create a CounterPage class that extends the StatefulWidget class. We will use the BlocProvider widget from the bloc package to provide the CounterBloc to the UI.

class CounterPage extends StatefulWidget {
@override
_CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
late CounterBloc _counterBloc;

@override
void initState() {
super.initState();
_counterBloc = CounterBloc();
}

@override
void dispose() {
_counterBloc.close();
super.dispose();
}

@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => _counterBloc,
child: Scaffold(
appBar: AppBar(
title: Text('Counter App'),
),
body: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Counter Value:',
),
Text(
'${state.value}',
style: Theme.of(context).textTheme.headline4,
),
],
),
);
},
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () =>
context.read<CounterBloc>().add(IncrementEvent()),
tooltip: 'Increment',
child: Icon(Icons.add),
),
SizedBox(height: 10),
FloatingActionButton(
onPressed: () =>
context.read<CounterBloc>().add(DecrementEvent()),
tooltip: 'Decrement',
child: Icon(Icons.remove),
),
],
),
),
);
}
}

The CounterPage class has a single property _counterBloc that we initialize in the initState method and dispose in the dispose method. We provide the CounterBloc to the UI using the BlocProvider widget. We use the BlocBuilder widget to rebuild the UI whenever the state changes.

The floatingActionButton property contains two FloatingActionButton widgets that send events to the BLoC when the user presses the buttons.

Testing the CounterBloc

Now that we have implemented the BLoC for our counter app, let’s write some tests to ensure that it works as expected.

Step 1: Create the CounterBlocTest

We will create a new file counter_bloc_test.dart and import the necessary packages and files.

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/counter_bloc.dart';

void main() {}

Step 2: Test the Initial State

The first test is to ensure that the initial state of the CounterBloc is CounterState(value: 0).

test('initial state is CounterState(value: 0)', () {
expect(CounterBloc().state, CounterState(value: 0));
});

Step 3: Test the Increment Event

The second test is to ensure that the CounterBloc emits a new state with a value of 1 when it receives an IncrementEvent.

test('emits CounterState(value: 1) when IncrementEvent is added', () {
final bloc = CounterBloc();
final expectedState = [CounterState(value: 1)];

expectLater(bloc.stream, emitsInOrder(expectedState));

bloc.add(IncrementEvent());
});

Step 4: Test the Decrement Event

The third test is to ensure that the CounterBloc emits a new state with a value of -1 when it receives a DecrementEvent.

test('emits CounterState(value: -1) when DecrementEvent is added', () {
final bloc = CounterBloc();
final expectedState = [CounterState(value: -1)];

expectLater(bloc.stream, emitsInOrder(expectedState));

bloc.add(DecrementEvent());
});

Step 5: Test the Multiple Events

The fourth test is to ensure that the CounterBloc can handle multiple events and emit the correct state.

test('emits correct states when multiple events are added', () {
final bloc = CounterBloc();
final expectedState = [
CounterState(value: 1),
CounterState(value: 2),
CounterState(value: 1),
CounterState(value: 0),
];

expectLater(bloc.stream, emitsInOrder(expectedState));

bloc.add(IncrementEvent());
bloc.add(IncrementEvent());
bloc.add(DecrementEvent());
bloc.add(DecrementEvent());
});

Step 6: Run the Tests

To run the tests, open a terminal window and navigate to the project directory. Then, run the following command:

flutter test

This will run all the tests in the project, including the ones we just wrote for the CounterBloc.

Conclusion

In this article, we have seen how to use the BLoC pattern in Flutter to manage the state of our apps. We have implemented a simple counter app and tested the CounterBloc to ensure that it works correctly. The BLoC pattern is a powerful tool for managing complex state in Flutter apps, and we encourage you to use it in your own projects.

--

--