State management: the key to build Flutter apps
Using state management to build your presentation layer
When we talk about state management it means we are looking to manage the way to share state between screens across the app.
We have different approaches to manage the states:
- Provider
- GetIt
- Riverpod
- GetX
- Redux
- BLoC
- and more…
What is a BLoC ?
A BLoC (Business Logic Component) is a pattern to separate the business logic from the UI and the idea is to maintain the state within the app.
It uses a reactive paradigm, it contains streams to manage the state. Let’s see the basic flow:
- The BLoC has the app state (stream of state)
- The widget is drawn using the state of the BLoC
- The user triggers an event
- The widget sends the event to the BLoC
- The BLoC do something regarding the event and emits a new state
- The widget is drawn again using the new state
So we have to get it clear:
- A BLoC receives Events and emits States.
- The UI receives States and emits Events.
A BLoC manages streams of state so we don’t have to worry about that. Once you get familiar with BLoC, you will love it. You will be able to create simple and powerful widgets, which will be updated when the app state changes and you don’t have to do anything to manage that state, just use it.
Why BLoC
As you can see, there are a lot of options to manage the state, and you will select one regarding the app you will create, regarding its complexity. I decided to use BLoC because it allows us to separate business logic from the user interface. It contributes to create a more maintainable, scalable and testable apps.
It could be more complicated to use it but once you understand it well, it will be easy and it will give us more Pros than Cons.
PROS
- Separate business logic from UI
- Easy to maintain
- Easy to test
- Improve scalability
- Easy to reuse business logic across your UI
- Performance
CONS
- It is not simple to use
- Learning curve
Where will our BLoC should be?
A BLoC will be on the Presentation layer. It will be a partner for the Widgets, and it will hide all the business logic, so the widgets will use it as a “black” box and they never see any business rule.
It provides a mechanism to get the widgets updated regarding the state of the app.
In the next image you can see how the Widgtes and the BLoCs stay together on the presentation layer.
You can improve your app architecture reading this article:
What does a BLoC do?
A BLoC will access to our business logic through our services. When a BLoC receives an Event, it will call the business service to process the event and then, once it has the results, it will emits a new state of the app.
The BLoC manages our application states and it will access to our business logic through our services. A BLoC receives an Event, do something with that (it resolves presentation logic, call some service to process the event) and then emits the new State of the App. There will be the UI listening the BLoC changes and it always renders itself according to the State that it receives without resolving any logic.
Let’s see how a BLoC looks like
When we use BLoCs, we have: the BLoC, its states and its events.
Let’s see an example of a BLoC, called BullListBloc, it will be responsible to manage the logic to get a list of entities and send it to the UI.
I use the package flutter_bloc which implements the BLoC pattern, so we dont have to worry about that.
I will create an event to get the entities, and its related states.
What do we have here:
- BullListBloc: the star of the night, the BLoC itself, it is in charge of getting the bulls.
- Fetch All Event: an event to indicate to the BLoC that we want to get all the bulls.
- Fetch by Name Event: an event to get all the bulls by a name.
- Loading State: the BLoC emits this event while it is getting the list or do something.
- Error State: the BLoC emits it when some error happens
- Success State: the BLoC emits the success to return the list of bulls
Let’s see the classes. I’ve just implemented one event so you can practice and create the other one.
BullListBloc
import '/src/presentation/bulls/bloc/bull_list_bloc_event.dart';
import '/src/presentation/bulls/bloc/bull_list_bloc_state.dart';
import '/src/domain/bull/bull.dart';
import '/src/domain/services/facade_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BullListBloc extends Bloc<BullListBlocEvent, BullListBlocState> {
final FacadeService _service = FacadeService() ;
BullListBloc():
super(BullListBlocState()) {
on<FetchAllRequested>(_onFetchAllRequested);
}
Future<List<Bull>> getEntities(){
return _service.getBulls();
}
//this method is triggered when the BLoC receive
//the event FetchAllRequested
Future<void> _onFetchAllRequested(
FetchAllRequested event,
Emitter<BullListBlocState> emit,
) async {
//emit loading status
emit(state.copyWith(status: () => BullListBlocStatus.loading));
await getEntities().then((entities)async{
//emit success status
emit(state.copyWith(
status: () => BullListBlocStatus.success,
entities: () => entities,
));
}).catchError( (error, stackTrace){
//emit error status
emit(state.copyWith(
status: () => BullListBlocStatus.failure,
message: () => error.toString()
));
});
}
}
As you can see, when the BLoC process the event FetchAllRequested, it emits 2 states: loading + ( success or error ).
So the UI could be drow
Let’s see the events and state classes and then we are going to see in detail the UI.
BullListBlocEvent, FetchAllRequested and FetchByNameRequested
abstract class BullListBlocEvent {
const BullListBlocEvent();
}
class FetchAllRequested extends BullListBlocEvent {
const FetchAllRequested();
}
class FetchByNameRequested extends BullListBlocEvent {
final String name;
const FetchByNameRequested(this.name);
}
Simple, right? You can add here all the request you want to send to the BLoC, and each of them will have the required parameters. In this example, we have the event to fetch bulls by name which has the name as parameter.
BullListBlocState
import 'package:equatable/equatable.dart';
import '/src/domain/bull/bull.dart';
enum BullListBlocStatus { initial, loading, success, failure }
extension BullListBlocStatusX on BullListBlocStatus {
bool get isInitial => this == BullListBlocStatus.initial;
bool get isSuccess => this == BullListBlocStatus.success;
bool get isError => this == BullListBlocStatus.failure;
bool get isLoading => this == BullListBlocStatus.loading;
}
class BullListBlocState extends Equatable {
final BullListBlocStatus status;
final List<Bull> entities;
String? message;
BullListBlocState({
this.status = BullListBlocStatus.initial,
this.entities = const [],
this.message="",
});
BullListBlocState copyWith({
BullListBlocStatus Function()? status,
List<Bull> Function()? entities,
String Function()? message
}) {
return BullListBlocState(
status: status != null ? status() : this.status,
entities: entities != null ? entities() : this.entities,
message: message != null ? message() : this.message,
);
}
@override
List<Object?> get props => [
status,
entities,
message
];
}
I defined an enum to have all the states and also there is an extension (BullListBlocStatusX) to know more about the current status when we use the states but it is not required.
At this point we have the BLoC with its states and events. The BLoC receives requests, talks with the service to get the bulls and emits states regarding its results. And now? we have to use it, let’s go!
Bloc Builder
First, we have to talk about how to use a BLoC. The BLoC implements the Observer design pattern, so there will be a subject emitting something and listeners waiting for the those things.
The library that we are using, flutter_bloc, provides classes to do that, let’s see:
- Bloc: the subject. Our BLoCs will extend this class.
- BlocBuilder: the listener. Our widgets will use it to know about app new state and rebuild themself regarding that current state.
So, now we have to create our widget that uses the BlocBuilder.
BullsPage
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '/src/presentation/bulls/bloc/bull_list_bloc_state.dart';
import '/src/domain/bull/bull.dart';
import '/src/presentation/bulls/bloc/bull_list_bloc.dart';
import '/src/presentation/bulls/widgets/bull_list.dart';
import '/src/presentation/widgets/loading_spinner.dart';
import 'bloc/bull_list_bloc_event.dart';
class BullsPage extends StatelessWidget {
const BullsPage({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<BullListBloc, BullListBlocState>(
builder: (context, state) {
Widget widget;
switch (state.status) {
case BullListBlocStatus.initial:
{
BlocProvider.of<BullListBloc>(context)
.add((const FetchAllRequested()));
widget = _buildLoadingPage(context);
}
break;
case BullListBlocStatus.loading:
{
widget = _buildLoadingPage(context);
}
break;
case BullListBlocStatus.failure:
{
widget = _buildErrorPage(context, state.message);
}
break;
case BullListBlocStatus.success:
{
widget = _buildPage(context, state.entities);
}
break;
default:
widget = _buildPage(context, List.empty());
}
;
return widget;
});
}
String getTitle(){
return "Bulls";
}
Widget _buildLoadingPage(BuildContext context) {
return LoadingSpinner();
}
Widget _buildPage(BuildContext context, List<Bull> bulls) {
return BullList(bulls);
}
Widget _buildErrorPage(BuildContext context, String msg) {
return BullErrorPage(msg);
}
}
As you can see, I rewrite the build method of the widget where I used the BlocBuilder. When the BlocBuilder receive the new state we will able to draw whatever we want. Into that builder there is a switch to build different widgets regarding the state:
- If it is “initial” state, we request the list.
- If it is “loading” state, we show a loading gift.
- If it is “success” state, we show the received entities.
- If it is “error” state, we show the error message.
I added an extra state called “initial” just for the first case, when you load the page, it will request the entities to the BLoC (I will explain this soon).
Initializing the BLoCs
Your widgets have to be related to BLoCs in some way, there are not any magic. The key are the BLoC providers.
BlocProvider
When you define a BLoC provider, you define it for a tree of widgets, so all of them will have access to the BLoC. Let’s see:
BlocProvider(
create: (BuildContext context) => BlocA(),
child: ChildA(),
);
Here we have the ChildA widget, this widget and all its subtree of widgets can access to the BLoC BlocA. And the way to access it is:
BlocProvider.of<BlocA>(context);
It seems to be difficult, but it is simple. Instead of use ChildA we will use BlocProvider(…. child: ChildA() … ). That is because BlocProvider is like a widget! so we have to embed our widget into it.
Then we can send an event to the BLoC from any widget defined in the same subtree as ChildA.
In our example of Bulls, you saw how we did it:
BlocProvider.of<BullListBloc>(context)
.add((const FetchAllRequested()));
We sent the event FetchAllRequest to the BLoC BullListBloc.
MultiBlocProvider
Additionally the flutter_bloc package includes the MultiBlocProvider class where you can define multiples providers for a widgets sub-tree:
MultiBlocProvider(
providers: [
BlocProvider<BlocA>(
create: (BuildContext context) => BlocA(),
),
BlocProvider<BlocB>(
create: (BuildContext context) => BlocB(),
),
BlocProvider<BlocC>(
create: (BuildContext context) => BlocC(),
),
],
child: ChildA(),
)
So the widgets sub-tree from ChildA will have access to BlocA, BlocB, and BlocC.
In the practice, I use to have the MultiBlocProvider defined in the main widget of my app, so all the widgets will have access to all the Blocs.
This is how my apps look like:
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<BullListBloc>(
create: (context) => DependencyManager().get<BullListBloc>(),
),
BlocProvider<CowListBloc>(
create: (context) => DependencyManager().get<CowListBloc>(),
),
BlocProvider<CountryListBloc>(
create: (context) => DependencyManager().get<CountryListBloc>(),
),
],
child: _buildMaterialApp(context),
);
}
You can find another BLoC example on my article where I talk about how to manage the Errors on Flutter:
Conclusion
- State management saves life.
- BLoC pattern is a powerful approach to manage the states, flutter_bloc package implements it.
- Have the MultiBlocProvider on the root of our App is a good practice.
- When you want to draw your widget regarding BLoC changes, use a BlocBuilder to build your widget.
- You’ve just seen the basics, you can do amazing design using BLoC’s.
Thanks for reading, Clap if you like it!
Let me know your comments below.