Uploaded by sampath

medium.com-Mastering Flutters BLoC Pattern Elevating Event-Driven State Management Part 1

advertisement
Mastering Flutter’s BLoC Pattern: Elevating Event-Driven
State Management (Part 1)
medium.com/@kumarsiddy/mastering-flutters-bloc-pattern-elevating-event-driven-state-management-part-1e0b871eadfbf
Purushotam Kumar
16 May 2023
Purushotam Kumar
Is this another Bloc tutorial that will cover the familiar concepts of event, state, and Bloc?
Why do we require yet another tutorial to implement the same functionality, where the UI
adds the event, the Bloc listens for the event and triggers the state, and the UI listens for
state changes? Indeed, we will be performing the same actions, but with an improved
methodology. We have come up with an elegant approach to efficiently manage the Bloc’s
state.
As our app grew at my previous organization, managing an increasing number of states and
sharing data among them became cumbersome. We needed a scalable solution to
accommodate our growing product needs. Finally, we developed an effective solution.
But before talking about the solution, I’ll talk about the basic concept of the bloc very briefly.
What is Bloc?
1/15
BLoC stands for Business Logic Components. But what does “business logic” mean?
Business logic refers to the core method or logic that calculates the data to be displayed to
the user. BLoC serves as a state management tool that isolates these crucial logics into a
distinct component from the UI, offering a means to connect the UI with these logics. Why do
we do this? It enhances the code reusability, scalability, and testability of the app.
Bloc’s Core components
Bloc has three important components. Let’s understand it one by one.
Basicallyan Event is a straightforward class that is incorporated into the Bloc to
communicate any interactions or changes that occur on the UI.
The State, also represented as a class, is emitted by the Bloc to signify the completion
of work accomplished by the Bloc. It serves as a notification mechanism for the Bloc’s
progress.
This class assumes responsibility for various aspects, including invoking the business
logic, receiving events, executing the appropriate tasks upon event reception, and
emitting a new state to indicate the outcome of the task, whether it was successful or
encountered a failure.
We’ll get to understand more about these concepts, once we’ll jump into our well-known
counter app with a little tweak as per our needs.
Project Setup
When writing this blog, I’m using flutter:3.7.12 and flutter_bloc: 8.1.2
Let’s first add flutter_bloc plugin inside pubspec.yaml file.
2/15
name:flutter_bloc_example
description:AnewFlutterproject.
publish_to:'none'
version:1.0.0+1
environment:
sdk:'>=2.19.6 <3.0.0'
dependencies:
flutter:
sdk:flutter
cupertino_icons:^1.0.2
# Flutter bloc package
flutter_bloc:^8.1.2
dev_dependencies:
flutter_test:
sdk:flutter
flutter_lints:^2.0.0
Let’s set up our project first.
Let’s see the project structure I'm following. You can choose your own structure. I’ve created
two folders bloc and ui inside lib folder. bloc will contain all the blocs and ui will have all the
screens.
Let’s define our main.dart file as following way:
void main() {
runApp(const MyApp());
}
{
const MyApp({super.key});
Widget build(BuildContext context) {
MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
); }}
title: ,
home: HomePage(),
3/15
Now, Let’s define our HomePage screen.
4/15
import'package:flutter/material.dart';
import'package:flutter_bloc/flutter_bloc.dart';
import'package:flutter_bloc_example/bloc/counter_bloc.dart';
{
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: Scaffold(
appBar: AppBar(
title: const Text('Flutter Bloc'),
),
body: _CounterScreen(),
),
);
}
}
{
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'You have pushed the button this many times:',
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 16,
),
child: Text(
// This needs to get updated using the new emitted state
// We'll come to this later
'0 Times',
style: Theme.of(context).textTheme.headlineMedium,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
5/15
onPressed: () => _onIncrement(context),
icon: const Icon(
Icons.add_circle,
size: 48,
color: Colors.blue,
),
),
const SizedBox(width: 16),
IconButton(
onPressed: () => _onDecrement(context),
icon: const Icon(
Icons.remove_circle,
size: 48,
color: Colors.blue,
),
),
],
),
],
),
);
}
void _onIncrement(
BuildContext context,
) {
// Incrementing by 1
BlocProvider.of<CounterBloc>(context).increment(
incrementNumber: 1,
);
}
_onDecrement(
BuildContext context, ) {
BlocProvider.of<CounterBloc>(context).decrement(
decrementNumber: ,
);
}}
Let’s understand what is happening here. We have wrapped Scaffold with a bloc provider, It
will push the CounterBloc’s instance inside the widget tree, so that it can be accessed from
inside by any child widget. We can see it in the below code block.
BlocProvider(
appBar: AppBar(
),
);
create: (_) => CounterBloc(),
title: Text(),
),
child: Scaffold(
body: _CounterScreen(),
And also, We have defined two buttons here. One to increment and another to decrement.
As soon as a button will be tapped, we will try to get the instance CounterBloc that has been
pushed by the provider to the tree and will call the corresponding method to
increment/decrement the counter. We can see it in the below code block.
6/15
_onIncrement(
BuildContext context, ) {
(context).increment(
incrementNumber: ,
);
BlocProvider.of<CounterBloc>
}
Now, Let’s define our counter_event class.
part of 'counter_bloc.dart';
abstract
{}
{
finalint incrementNumber;
CounterIncrementEvent({
requiredthis.incrementNumber,
});
}
{
finalint decrementNumber;
CounterDecrementEvent({
.decrementNumber,
});}
Here we have defined one abstract class CounterEvent. This is abstract because we don’t
want to use it directly. Every other event will extend this class. CounterIncrementEvent and
CounterDecrementEvent is extending CounterEvent.
Now, Let’s define our counter_state class.
7/15
part of 'counter_bloc.dart';
abstract {
final CounterStateStore store;
CounterState({
requiredthis.store,
});
}
{
CounterInitialState({
requiredsuper.store,
});
}
{
CounterUpdateState({
requiredsuper.store,
});
}
@immutable
{
finalint? counter;
const CounterStateStore({
this.counter,
});
CounterStateStore copyWith({
counter: counter ?? .counter,
);
counter,
}}
}) {
CounterStateStore(
Here For state class, we are following the same pattern as the definition of the event.
CounterState is abstract.
There is a difference in how the CounterState is defined. It includes a final field variable
called CounterStateStore. This change is not commonly found in other tutorials. However,
this small change has had a significant impact on our code.
To understand it better, let’s look at the definition of the CounterStateStore class. It is marked
as immutable, meaning you cannot change the value of its variables without creating a new
object. CounterStateStore should include variables that should not be lost while changing the
8/15
state.
In our case, we are manipulating the value of the counter and we don’t want to lose its value
when changing the state. That’s why the counter variable is defined inside the store class.
Before discussing the solution we found, there’s another important thing to mention. We have
a method called copyWith inside the CounterStateStore. This method is responsible for
returning a new object with the previous counter value copied.
Later on, in the CounterBloc class, we will utilize this copyWith method. We will continue our
discussion about the solution we arrived at in that context.
If the CounterStateStore contains a large number of variables, it can become
challenging to write and manage the copyWith method. In the second part of this blog,
we will explore how we can address this issue using different packages.
Now, Let’s define our bloc class.
9/15
import'package:flutter/foundation.dart';
import'package:flutter_bloc/flutter_bloc.dart';
part'counter_event.dart';
part'counter_state.dart';
<, > {
CounterBloc()
: super(
CounterInitialState(
store: const CounterStateStore(
counter: 0,
),
),
) {
on<CounterIncrementEvent>(_onCounterIncrement);
on<CounterDecrementEvent>(_onCounterDecrement);
}
Future<void> _onCounterIncrement(
CounterIncrementEvent event,
Emitter<CounterState> emit,
) async {
final counter = state.store.counter! + event.incrementNumber;
emit(
CounterUpdateState(
store: state.store.copyWith(
counter: counter,
),
),
);
}
Future<void> _onCounterDecrement(
CounterDecrementEvent event,
Emitter<CounterState> emit,
) async {
final counter = state.store.counter! - event.decrementNumber;
emit(
CounterUpdateState(
store: state.store.copyWith(
counter: counter,
),
10/15
),
);
}
void increment({
requiredint incrementNumber,
}) {
add(
CounterIncrementEvent(
incrementNumber: incrementNumber,
),
);
}
decrement({
decrementNumber,
decrementNumber: decrementNumber,
),
}) {
);
add(
}}
CounterDecrementEvent(
Let’s begin with the constructor of the bloc. In the constructor, we are initializing the
counterBloc with its initial state. This sets the initial data for the bloc. Additionally, we are
registering all the event change listeners in this constructor. These event change listeners
will be triggered whenever the UI triggers an event.
CounterBloc()
: (
CounterInitialState(
store: CounterStateStore(
counter: ,
),
),
<CounterIncrementEvent>(_onCounterIncrement); <CounterDecrementEvent>
(_onCounterDecrement);}
) {
Let’s understand the methods defined inside bloc, which will be called as soon as the user
will tap on the increment/decrement button on the UI.
increment({
incrementNumber,
incrementNumber: incrementNumber,
}) {
),
add(
); }
CounterIncrementEvent(
Here, UI will pass incrementedNumber and as soon as this method will be called, we’ll add
the events to the bloc. And the listener will call the _onCounterIncrement method after
adding the event. Let’s understand what’s cooking inside this _onCounterIncrement method.
Future<> _onCounterIncrement(
CounterIncrementEvent event,
Emitter<CounterState> emit, ) {
counter = state.store.counter! +
event.incrementNumber;
emit(
CounterUpdateState(
store:
state.store.copyWith(
counter: counter,
),
),
);
}
This method performs a straightforward task. It increments the counter, sets the incremented
value in the store, and emits the new state.
Now, We’ll talk about the final solution we arrived at. We have already defined copyWith
method inside CounterStateStore, By utilizing this defined method, we are going to have the
capability to retain the previous value without passing it in the state. This concise method
11/15
grants us the freedom to streamline state updates efficiently without losing any data or
without having the burden to pass all the parameters again.
Now, It is time to update our counter_page with a builder around it, which reacts to the state
change.
12/15
import'package:flutter/material.dart';
import'package:flutter_bloc/flutter_bloc.dart';
import'package:flutter_bloc_example/bloc/counter_bloc.dart';
classCounterPageextendsStatelessWidget {
constCounterPage({super.key});
@override
Widgetbuild(BuildContext context) {
returnBlocProvider(
create: () =>CounterBloc(),
child: Scaffold(
appBar: AppBar(
title: constText('Flutter Bloc'),
),
body: _CounterScreen(),
),
);
}
}
class_CounterScreenextendsStatelessWidget {
@override
Widgetbuild(BuildContext context) {
// We have added builder here
returnBlocBuilder<CounterBloc, CounterState>(
// This builder will get called on each state change
builder: (context, state) {
returnCenter(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
constText(
'You have pushed the button this many times:',
),
Padding(
padding: constEdgeInsets.symmetric(
vertical: 16,
),
child: Text(
// Now this text will have the value from the state's store
'${state.store.counter ?? 0} Times',
style: Theme.of(context).textTheme.headlineMedium,
),
),
Row(
13/15
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () =>_onIncrement(context),
icon: constIcon(
Icons.add_circle,
size: 48,
color: Colors.blue,
),
),
constSizedBox(width: 16),
IconButton(
onPressed: () =>_onDecrement(context),
icon: constIcon(
Icons.remove_circle,
size: 48,
color: Colors.blue,
),
),
],
),
],
),
);
},
);
}
void_onIncrement(
BuildContext context, ) {
// Incrementing by 1
BlocProvider.of<CounterBloc>(context).increment(
incrementNumber: 1,
);
}
() {
.<>(context).(
: ,
);
}}
In the above code, UI is encapsulated within a BlocBuilder, utilizing the builder method
that is invoked with each state emitted by the Bloc. Voila! As you tap on the buttons, the
counter value displayed on the screen promptly updates.
Explore the code base of this project by visiting the GitHub repository.
Conclusion:
This concludes Part 1 of the Bloc tutorial, where we gained a solid understanding of the
fundamental concepts of event, Bloc, and state in action. And we also understood how a
small change can solve a bigger problem. In Part 2, we will delve deeper into utilizing various
14/15
packages(such as get_it, freezed, dartz, build_runner, and mason) that significantly
simplify the state management process using bloc.
Thank you for reading this blog, and I hope you found it informative and engaging. Stay
tuned for more updates and insights on the ever-evolving world of Dart and Flutter.
If I got something wrong? Let me know in the comments. I would love to improve.
Clap
👏 If this article helps you.
If you require any assistance or consultation for your project, please feel free to email me at
purushotam.kr@hotmail.com
15/15
Download