Flutter -BLoC Pattern
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 CounterBloc
class 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.