In this guide, we will learn a complete guide Bloc as state management in a flutter. When building an application, Flutter is the easiest and most powerful framework. But building an application without any strong architecture is like building a house without any planning and blueprints. We won’t understand the uses of architecture when building small applications. when it comes to building a big production level application where you have many screens, animations, methods, classes, etc, without any proper architecture you will end up in a state where everything is messed up and you don’t know how all the components, classes, methods are communicating and functioning. So it is very necessary to maintain the code to make code more readable and testable, and easily trackable when designing and developing this kind of big application. There are many different packages available, and all have their own way to handle application states.
In this article, we are going to talk about
Bloc
and a complete Guide on it. Bloc is not just state management, but it’s also an architectural design pattern that helps us to build production-level applications.
What is Bloc?
- Bloc stands for Business Logic Component.
- It helps to separate business logic from the presentation layer and enables a developer to reuse code more efficiently
- created and maintained by Felix Angelo
- It helps in managing state and make access to data from a central place in our project.
- relies on
events
to triggerstate
changes rather than functions. - receive
events
and convert the incomingevents
into outgoingstates
. - the
Bloc
class extendsBlocBase
, we have access to the current state of the bloc at any point in time via thestate
getter just like inCubit
. - Blocs should never directly
emit
new states. Instead, every state change must be output in response to an incoming event within anEventHandler
. - One key differentiating factor between
Bloc
andCubit
is that becauseBloc
is event-driven, we are also able to capture information about what triggered the state change. - they allow us to override
onEvent
which is called whenever a new event is added to theBloc
. Bloc
has an event sink that allows us to control and transform the incoming flow of events.
Pros of BLoC
- Easy to separate UI from logic
- Easy to test code
- Easy to reuse code
- Good performance
Cons of BloC
- Technically, you need to use streams in both directions, creating a lot of boilerplate. However, many people cheat this by using streams only from the backend to the UI, but when events occur they’re simply calling functions directly instead of feeding those events into a sink.
- More boilerplate than ScopedModel or Provider, but it can be worth it for anything larger than a small app.
The above diagram shows how the data flow from UI to the Data layer and vice versa. BLOC will never have any reference for the Widgets on the UI Screen. The UI screen will only observe changes coming from the BLOC class.
The Bloc is distinguished into four layers.
1. UI (Presentation Layer):
All the component(Widgets) of the app which is visible to the user is defined here.
- The presentation layer’s responsibility is to figure out how to render itself based on one or more bloc states.
- In addition, it should handle user input and application lifecycle events. The presentation layer will have to figure out what to render on the screen based on the state from the bloc layer.
2. Bloc (Business Logic Layer):
It acts as a middle man between UI and Data layer.
- Bloc takes an event triggered by the user (ex: GetWeatherData button press, Submit form button press, etc) as an input, and responds back to the UI with the relevant state.
- The business logic layer’s responsibility is to respond to input from the presentation layer with new states.
- This layer can depend on one or more repositories to retrieve data needed to build up the application state.
- The business logic layer is notified of events/actions from the presentation layer and then communicates with the repository in order to build a new state for the presentation layer to consume.
3. Data Layer:
This layer has further two parts. Repository and Data Provider.
3.1 Data Provider:
This layer retrieves/fetches the raw data from different data sources (ex: different APIs, DBs, networks, Shared preferences, etc).
- For example: If you are building a weather app. Then you might use external APIs like OpenWeatherAPI, from where you will get raw data.
- You can have
GET
,POST
,DELETE
, etc methods inside this class. - The data provider’s responsibility is to provide raw data. The data provider should be generic and versatile.
- The data provider will usually expose simple APIs to perform CRUD operations.
- We might have a
createData
,readData
,updateData
, anddeleteData
the method as part of our data layer.
import 'package:http/http.dart' as http;
class DataService {
Future<http.Response?> getPosts() async {
const _baseUrl = 'jsonplaceholder.typicode.com';
try {
final url = Uri.https(_baseUrl, '/posts');
final response = await http.get(url);
return response;
} catch (e) {
rethrow;
}
}
}
3.2 Repository:
This layer contains one or more than one Data providers.
- The transformation is done on the
raw
data returned by the Data Provider in this layer. (Forex: converting themraw
into some kind of Model). - Bloc communicates with this layer when the user requests the data.
- This layer requests raw data from the Data Provider and after that, this layer performs some kind of transformation
- The repository layer is a wrapper around one or more data providers with which the Bloc Layer communicates.
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'package:bloc_api/Model/post.dart';
import 'package:bloc_api/Service/data_service.dart';
class ApiRepository {
final DataService dataService;
ApiRepository({
required this.dataService,
});
Future<List<Post>> getPostsList() async {
final response = await dataService.getPosts();
if (response!.statusCode == 200) {
final json = jsonDecode(response.body) as List;
final posts = json.map((e) => Post.fromJson(e)).toList();
return posts;
} else {
throw Exception('Failed to load Posts');
}
}
}
Two important terms that we need to understand: Event and State.
Event: Event is nothing but different actions (button click, submit, etc) triggered by the user from UI. It contains information about the action and gives it to the Bloc to handle. Events are the input to a Bloc. They are commonly added in response to user interactions such as button presses or lifecycle events like page loads. Events are the input to a Bloc. They are commonly added in response to user interactions such as button presses or lifecycle events like page loads.
State: The UI will update according to State
what was received from the Bloc. For example, there could be different kinds of states:
LoadingState - Will Show Progress Indicator(adsbygoogle = window.adsbygoogle || []).push({});
LoadedState - Will Show Actual widget with data
ErrorState - Will show an error that something went wrong.
How does it work?
When you use flutter bloc you are going to create events to trigger the interactions with the app and then the bloc in charge is going to emit the requested data with a state, in a real example it will be like that:
1- The user clicks on a button to get a list of games.
2- The event is triggered and it informed to the bloc that the user wants a list of games.
3- The bloc is going to request this data ( from a repository for example, which is in charge of connecting to the API to get the data).
4- When the bloc has the data it will determine if the data is a Success or is Error, and then it’s going to emit a state.
5- The view is going to be listening to all the possible states that the bloc could emit to react to them. For instance, if bloc emits Success as a state the view is going to rebuild it with a list of games, but if the state is Error the view is going to rebuild with an error message or whatever you want to show.
Implementing Bloc
Before we dive into implementation VS Code has a very handy extension for Bloc which is named a bloc. Make sure we have installed it.
1: Create a Flutter Application
flutter create bloc_practise
2: Go to pubspec.yaml
and add flutter_bloc packages inside dependencies
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.0.1
3: Create bloc
files: Now right-click on the lib
folder of our application, here we’ll see Bloc: New Bloc
option if we’ve installed the bloc
extension
After clicking on this, the dialog will pop up for giving the name to the bloc. Give the name and press Enter.
Now If we see in the lib
directory the folder named bloc
is created and it has 3 different files – counter_bloc
, counter_event
, and counter_state
Counter_bloc.dart
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'counter_event.dart';
part 'counter_state.dart';
class CountersBloc extends Bloc<CountersEvent, CountersState> {
CountersBloc() : super(CountersInitial()) {
on<CountersEvent>((event, emit) {
// TODO: implement event handler
});
}
}
The counter_bloc.dart
class is a bridge between our UI and the Data class, In other words, this class will handle all the Events triggered by the User and sends the relevant State back to the UI.
- We are extending our
counterbloc
classBloc
which takes two thingscounterEvent
andcounterState
. As the name suggests, they handle State and Events respectively. - In the second line, the
constructor
of our class is created. In this, we need to provide ainitial
state. It’s not going to do anything. It simply represents that the app is now in its initial stage and nothing has happened yet.
counter_event.dart
part of 'counter_bloc.dart';
@immutable
abstract class CounterEvent {}
class CounterIncrement extends CounterEvent {}
class Counterdecrement extends CounterEvent {}
- In this class, we define different kinds of events by extending the abstract event class.
- For example, when the user presses a button, there is 2 button that is increment and decrement button.
- Now let’s implement
weather_state.dart
class
counter_state.dart
// ignore_for_file: public_member_api_docs, sort_constructors_first
part of 'counter_bloc.dart';
@immutable
class CounterBlocState {
final int counter;
const CounterBlocState({
required this.counter,
});
}
class CounterInitial extends CounterBlocState {
const CounterInitial() : super(counter: 0);
}
class IncrementState extends CounterBlocState {
const IncrementState(int increment) : super(counter: increment)
}
class DecrementState extends CounterBlocState {
const DecrementState(int increment) : super(counter: increment);
}
In this class, we define different kinds of states (For example, initial state, increment state, decrement state).
Back to counter_bloc.dart
:
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
part 'counter_event.dart';
part 'counter_state.dart';
class CounterBloc extends Bloc<CounterEvent, CounterBlocState> {
CounterBloc() : super(const CounterInitial()) {
on<CounterIncrement>((event, emit) {
emit(IncrementState(state.counter + 1));
});
on<Counterdecrement>((event, emit) {
emit(DecrementState(state.counter - 1));
});
}
}
4: How to access bloc
data in UI?
- we have created the bloc and implemented all the functionalities we need to somehow provide this bloc to the widget tree so that we can use the data and display it on the screen.
- Before that, we have to understand different Widgets provided by bloc:
- BlocProvider
- BlocBuilder
- BlocListener
- BlocConsumer
- RepositoryProvider
1. BlocProvider
- provides a bloc to its children (i.e Widgets).
- used as a dependency injection (DI) widget so that a single instance of a bloc can be provided to multiple widgets within a subtree.
- We have to put it at the place from where all the children can access the bloc.
- So let’s wrap the BlocProvider inside MaterialApp
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Bloc',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: BlocProvider(
create: (context) =>
CounterBloc()
),
child: const HomeScreen(),
),
);
}
- Now we can access our bloc via
BlocProvider.of<counterBloc>
- BlocProvider has one
lazy
parameter. By default, it’strue
. It is used to lazily load thebloc
. It means whenever anyone tries to use thembloc
then it will be initialized. - To override this functionality we can change the value of
lazy
tofalse
. - Now we might have asked a question, what if we have multiple blocs. How can we provide all the bloc from the
main.dart
? we will use a MultiBlocProvider for it.
MultiBlocProvider:
It provides us a MultiBlocProvider widget that takes a List of Bloc and provides it to its children. Let me demonstrate
MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => CounterCubit(),
),
BlocProvider(
create: (context) => CounterBloc(),
),
BlocProvider(
create: (context) => ThemeCubit(),
),
],
child: MyApp(
appRouter: AppRouter(),
),
),
),
2. BlocBuilder:
- BlocBuilder is a widget that helps Re-building the UI based on State changes.
- In our case we want our UI to update the state when the user presses the
incremnet and decrement
button. - BlocBuilder builds the UI every single time state changes
- So, it’s very necessary to place BlocBuilder around the Widget that we want to rebuild.
- we can also wrap the whole Widget inside the BlocBuilder (i.e around the Scaffold), but it’s not a good way. Because think about the time and processing power that will be consumed when your whole widget tree rebuilds just to update a Text widget inside the tree. So make sure you wrap the BlocBuilder around the widget that needs to be rebuilt when the state changes.
body: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return ...
}
)
- BlocBuilder takes two things
bloc
, andstate
. builder
the function is required which takes two parameters. context and thestate
which is of typeWeatherState
in our case. And it should return Widget in response.- We can explicitly provide the
bloc
in BlocBuilder bypassing the bloc insidebloc
the property of BlocBuilder
BlocBuilder<CounterBloc, CounterState>(
bloc: blocA, // provide the local bloc instance
builder: (context, state) {
return ...
}
)
- If the bloc parameter is omitted, BlocBuilder will automatically perform a lookup using BlocProvider and the current BuildContext.
- Only specify the bloc if you wish to provide a bloc that will be scoped to a single widget and isn’t accessible via a parent BlocProvider and the current BuildContext.
buildWhen
the parameter takes the previous bloc state and current bloc state and returns a boolean. IfbuildWhen
returns true,builder
will be called with state and the widget will rebuild. IfbuildWhen
returns are false,builder
will not be called with state and no rebuild will occur.
BlocBuilder<CounterBloc, CounterState>(
buildWhen: (previousState, state) {
},
builder: (context, state) {
return ...
}
)
- we can access your Bloc by – BlocProvider.of(context) or context.
context.read<WeatherBloc>().add(Decrement());
// or
BlocProvider.of<WeatherBloc>(context)add(Decrement())
(adsbygoogle = window.adsbygoogle || []).push({});
3. BlocListener:
- As the name suggests, this will listen to any state change as BlocBuilder does.
- But instead of building the widget like BlocBuilder, it takes one function,
listener
which is called only once per state, not including the initial state. - Example: Navigation, Showing a SnackBar, Showing a Dialog, etc…
- It also has a
bloc
parameter. Only specifybloc
if you wish to provide a bloc that is otherwise not accessible via BlocProvider and the current BuildContext. - The
listenWhen
the parameter is the same as BlocBuilder‘sbuildWhen
but for Listener. - The whole idea of BlocListener is – It is not responsible for building/updating the widget as BlocBuilder does.
- It only listens to the state changes and performs some operations. The operation could be (Navigating to other screens when state changes, Showing Snackbar on a particular state, etc).
- Let’s say we want to show a snackbar on IncrementState state –
- Wrap the body inside BlocListener.
BlocListener<CounterBloc, CounterState>(
listener: (context, state) {
if (state is IncrementState) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Successfully Increment'),
duration: const Duration(milliseconds: 300),
));
} else {
if (state is DecrementState) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Successfully Decrement'),
duration: const Duration(milliseconds: 300),
));
}
}
},
child: builder: (context, state) {
return Text(
state.counter.toString(),
style:
const TextStyle(fontSize: 50, fontWeight: FontWeight.bold),
);
},
},
),
),
),
4. MultiBlocListener:
It is a Flutter widget that merges multiple BlocListener widgets into one. MultiBlocListener improves the readability and eliminates the need to nest multiple BlocListeners
BlocListener<BlocA, BlocAState>(
listener: (context, state) {},
child: BlocListener<BlocB, BlocBState>(
listener: (context, state) {},
child: BlocListener<BlocC, BlocCState>(
listener: (context, state) {},
child: ChildA(),
),
),
)
TO
MultiBlocListener(
listeners: [
BlocListener<BlocA, BlocAState>(
listener: (context, state) {},
),
BlocListener<BlocB, BlocBState>(
listener: (context, state) {},
),
BlocListener<BlocC, BlocCState>(
listener: (context, state) {},
),
],
child: ChildA(),
)
5. BlocConsumer:
- As of now we are building a widget using BlocBuilder and showing the snackbar using BlocListener. Is there any easy way to combine both in a single widget?
- Bloc provides a BlocConsumer widget, which combines both BlocListener and BlocBuilder.
- So instead of writing BlocListener and BlocBuilder separately, we can do –
BlocConsumer<CounterBloc, CounterBlocState>(
listener: (context, state) {
if (state is IncrementState) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Successfully Increment'),
duration: Duration(milliseconds: 300),
),
);
} else {
if (state is DecrementState) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Successfully Decrement'),
duration: Duration(milliseconds: 300),
),
);
}
}
},
builder: (context, state) {
return Text(
state.counter.toString(),
style:
const TextStyle(fontSize: 50, fontWeight: FontWeight.bold),
);
},
),
RepositoryProvider:
- It is the same widget as BlocProvider.
- But the main difference is that BlocProvider provides a single instance of
bloc
to its children whereas RepositoryProvider providesrepositories
to its children. - It is used as dependency injection (DI) widget so that a single instance of a repository can be provided to multiple widgets within a subtree.
RepositoryProvider(
create: (context) => CounterRepository(),
child: ChildWidget(),
);
The widget can access this repository by –
context.read<CounterRepository>();
// or
RepositoryProvider.of<CounterRepository>(context)
MultiRepositoryProvider:
As the name suggests, it provides multiple repositories. example:
MultiRepositoryProvider(
providers: [
RepositoryProvider<RepositoryA>(
create: (context) => RepositoryA(),
),
RepositoryProvider<RepositoryB>(
create: (context) => RepositoryB(),
),
RepositoryProvider<RepositoryC>(
create: (context) => RepositoryC(),
),
],
child: ChildA(),
)
BlocSelector:
- It is a Flutter widget that is analogous to
BlocBuilder
but allows developers to filter updates by selecting a new value based on the current bloc state. - The selected value must be immutable in order for
BlocSelector
to accurately determine whetherbuilder
should be called again. - If the
bloc
the parameter is omitted,BlocSelector
will automatically perform a lookup usingBlocProvider
and the currentBuildContext
.
BlocSelector<BlocA, BlocAState, SelectedState>(
selector: (state) {
// return selected state based on the provided state.
},
builder: (context, state) {
// return widget here based on the selected state.
},
)
For the Code Snippet Download from here
Also Read: