BLoC and Redux State Management
In Flutter, there are various state management patterns available, each with its own strengths and characteristics. Two popular patterns in the Flutter community are the BLoC (Business Logic Component) pattern and the Redux pattern. Both patterns provide solutions for managing state in Flutter applications effectively.
The BLoC pattern focuses on separating business logic from presentation and allows for reactive and testable code. It employs the use of events, streams, and sinks to handle user actions and update application state accordingly. The BLoC pattern requires a bit more boilerplate code and has a steeper learning curve but offers excellent separation of concerns, responsiveness, testability, and scalability.
Redux is a well-known state container model that is frequently utilized in web and mobile applications. It ensures unidirectional data flow, making state changes easier to understand and debug. The central store strategy is used by Redux, in which the entire application state is saved in a single immutable object called the store.
Both the BLoC and Redux patterns have their advantages and use cases. In this article we will look at them individually and understand how they work using flutter applications.
Business Logic Component (BLoC) Pattern
BLoC is popular in the Flutter community because of its separation of concerns, responsiveness, testability and scalability. However, it may require more boilerplate code than other state management approaches and has a steeper learning curve.
There are several core concepts to understand when using BLoC in Flutter:
- Events: events signify user activities or other actions that can alter the application’s state. Events are typically represented as simple data classes.
- Bloc: a Bloc is a class that takes in events, processes them, and produces a new state. It is in charge of controlling the application’s state and responding to user input.
- State: state represents the current state of the application. It is typically represented as an immutable data class.
- Stream: a stream is a collection of asynchronous events that may be monitored for modifications. In the context of BLoC, Streams are used in BLoC to describe the application’s state at any given time.
- Sink: a Sink is a Stream controller that can be used to send events to a stream. In the context of BLoC, a Sink is used to send events to the Bloc for processing.
- StreamController: StreamController is used to construct and manage streams. In the context of BLoC, a StreamController is used to manage the stream(s) of events that are sent to the Bloc.
- BlocBuilder: BlocBuilder is a widget provided by the flutter_bloc package that helps to connect the Bloc to the user interface. It listens to changes in the state of the Bloc and rebuilds the UI accordingly.
- BlocProvider: The flutter_bloc package has a widget called BlocProvider that adds a Bloc to the widget tree. It ensures that the Bloc is created only once and is accessible to all the widgets in the subtree.
These are some of the core concepts that are essential to understanding the BLoC pattern in Flutter. By understanding these concepts, you can create well-architected Flutter applications that are easy to maintain and test.
BLoC (capitalized) refers to the Business Logic Component pattern, which is a state management pattern while bloc (lowercase) is a term that is often used to refer to an instance of the Bloc class that implements the BLoC pattern.
So while the two terms are related, they refer to different concepts — BLoC refers to a design pattern, while bloc refers to an instance of a class that implements that pattern. We are going to be using both words so it’s good to recognize the difference.
Create an application using the BLoC pattern
We will be creating a simple application of a container that changes its color from red to blue using bloc, events, and states. To use the Bloc pattern for state management, we must add the flutter_bloc package to our project’s dependencies.
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.2
After adding the package to our project, we are going to create a folder inside our lib folder called bloc, this folder is going to hold our color_bloc.dart file, color_ event.dart file and color_state.dart file, you will create those three files inside the bloc folder.
Once we have succeeded in creating the folder and files, we are going to proceed to define the events in the Color_event.dart file.
part of 'color_bloc.dart';
@immutable
abstract class ColorEvent {}class InitialEvent extends ColorEvent {
InitialEvent();
}class ColorToBlue extends ColorEvent {
ColorToBlue();
}class ColorToRed extends ColorEvent {
ColorToRed();
}
In this part of the code, we define a set of classes that represent events in a BLoC.
The first line part of color_bloc.dart'
; indicates that the code is part of a larger file called color_bloc.dart which is going to contain the implementation of the BLoC.
The @immutable
annotation is used to indicate that instances of the classes defined below are immutable and cannot be changed once created. This helps to ensure that the state of the application is not accidentally modified.
The abstract class ColorEvent {}
defines an abstract class called ColorEvent
that will be used as a base class for all the events in the BLoC. This class does not contain any implementation and cannot be instantiated directly.
The three classes that follow — InitialEvent
, ColorToBlue
, and ColorToRed
– are concrete classes that extend the ColorEvent
class. They represent different types of events that can be sent to the BLoC to trigger a state change.
The InitialEvent
class represents the initial event that is sent to the BLoC when it is first created. It is typically used to initialize the state of the application.
The ColorToBlue
and ColorToRed
classes represent events that can be sent to the BLoC to change the color state of the application. They do not contain any additional information beyond the fact that the color should be changed to blue or red, respectively.
These classes define the events that can be sent to the BLoC and help to ensure that the state of the application is updated in a predictable and controlled way.
Let us define the states that the Bloc will handle.
part of 'color_bloc.dart';
@immutable
abstract class ColorState {}class ColorInitial extends ColorState {}class ColorUpdateState extends ColorState {
bool? initialState;
ColorUpdateState({
this.initialState,
});
}
This code defines a set of classes that represent the state of the bloc. We have already explained part of ‘color_bloc.dart
and @immutable
.
The abstract class ColorState {}
defines an abstract class called ColorState that will be used as a base class for all the states in the bloc. This class does not contain any implementation and cannot be instantiated directly.
The two classes that follow — ColorInitial
and ColorUpdateState
– are concrete classes that extend the ColorState
class. They represent different states that the bloc will be in.
The ColorInitial
class represents the initial state of the bloc. It is used to indicate that the bloc has just been created and has not yet received any events.
The ColorUpdateState
class represents a state where the color of the application has been updated. It contains a boolean value called initialState
that indicates the current state of the color.
The ColorUpdateState
class also has a named constructor that takes an optional initialState
parameter. This constructor can be used to create new instances of the ColorUpdateState
class with a specified initialState
value.
These classes define the different states that the bloc can be in and help to ensure that the state of the application is updated in a predictable and controlled way.
We then define a Bloc class that handles the events and updates the state of the application.
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
part 'color_event.dart';
part 'color_state.dart';class ColorBloc extends Bloc<ColorEvent, ColorState> {
bool initState = true;
ColorBloc() : super(ColorInitial()) {
on<ColorEvent>((event, emit) {
//Implement an event handler
});
on<InitialEvent>((event, emit) {
// implement event handler
emit(ColorUpdateState(initialState: initState));
});
on<ColorToBlue>((event, emit) {
//implement event handler
initState = true;
emit(ColorUpdateState(initialState: initState));
});
on<ColorToRed>((event, emit) {
initState = false;
emit(ColorUpdateState(initialState: initState));
});
}
}
The ColorBloc
class takes two type parameters, ColorEven
t and ColorState
, which represent the events that can be sent to the bloc and the states that can be emitted by the bloc, respectively.
We also imported the meta package and defined two-part directives that import the ColorEvent
and ColorState
classes from their separate files.
In the constructor of the ColorBloc
class, the super keyword is used to call the constructor of the Bloc class and initialize the initial state of the bloc to ColorInitial()
.
The on method is used to register event handlers for different types of events. For example, when a ColorToBlu
e event is received, the ColorUpdateState
state is emitted with the initialState
field set to true.
Similarly, when a ColorToRed
event is received, the ColorUpdateState
state is emitted with the initialState
field set to false.
We are using the initState
boolean variable to keep track of the current state of the bloc. When the ColorToBlue
event is received, the initState
variable is set to true, and when the ColorToRed
event is received, the initState
variable is set to false.
So, overall, what we just did is define a ColorBloc
class that can handle different types of events and emit corresponding states based on the current state of the bloc.
To use ColorBloc
in the widget tree, we must wrap it with the BlocProvider
widget.
lass MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
//Wrap with the BlocProvider widget
home:
BlocProvider(create: (_) => ColorBloc(), child: const ColorScreen()),
);
}
}
In the previous code, we wrapped the MaterialApp
widget with the BlocProvider
widget.
BlocProvider
is a widget provided by the bloc package. It is used to provide a Bloc instance to a subtree of widgets, making it available to be used by all widgets in that subtree.
We have also provided the CounterBloc
instance to the create parameter of the BlocProvider
widget.
It’s time to manage the state of our app using the states and events created.
class ColorScreen extends StatefulWidget {
const ColorScreen({super.key});
@override
State<ColorScreen> createState() => _ColorScreenState();
}class _ColorScreenState extends State<ColorScreen> {
@override
void initState() {
// TODO: implement initState
super.initState();
context.read<ColorBloc>().add(InitialEvent());
} @override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('colorz'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocConsumer<ColorBloc, ColorState>(listener: (context, state) {
print(state);
}, builder: (context, state) {
if (state is ColorUpdateState) {
return Column(
children: [
Container(
width: 200,
height: 200,
color: state.initialState == true ? Colors.blue : Colors.red,
),
SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () =>
context.read<ColorBloc>().add(ColorToBlue()),
child: Container(
width: 50,
height: 30,
decoration: BoxDecoration(
color: Colors.blue,
),
child: Center(child: Text('blue')),
),
),
const SizedBox(
width: 20,
),
GestureDetector(
onTap: () =>
context.read<ColorBloc>().add(ColorToRed()),
child: Container(
width: 50,
height: 30,
decoration: BoxDecoration(
color: Colors.red,
),
child: Center(child: Text('red')),
),
),
],
),
],
);
} else {
return Container();
}
}),
],
),
);
}
}
In this code, We are using the BlocConsumer
widget from the flutter_bloc
library to consume the state changes emitted by a ColorBloc
instance in ColorScreen
widget .
The BlocConsumer
widget has two builder functions: a builder function and a listener function.
The builder function is a function responsible for creating the user interface (UI) based on the current state of the BLoC. On the other hand, the listener function is called whenever a new state is emitted by the BLoC, allowing the UI to be updated accordingly.
To put it simply, the builder function builds the initial UI, while the listener function is responsible for updating the UI whenever there is a change in the state of the BLoC.
This helps keep the UI in sync with the underlying data and provides a clear separation of concerns between the presentation and business logic.
The builder function takes in the context and the current state of the ColorBloc
as arguments. If the current state is an instance of ColorUpdateState
, the UI is built with a Container widget that displays a colored box based on the value of the initialState
field of the ColorUpdateState
instance. So, if the initialState
is true, the box is colored blue, otherwise it is colored red.
We also have two GestureDetector
widgets that allow the user to change the color of the box by tapping on them. Tapping on the blue GestureDetector
widget dispatches a ColorToBlue
event to the ColorBloc
, while tapping on the red GestureDetector
widget dispatches a ColorToRed
event.
The listener function in this code simply prints the new state every time a state change occurs in the ColorBloc
. Finally, in the initState
method of the _ColorScreenState
class, a new InitialEvent
is dispatched to the ColorBloc
using the context.read().add(InitialEvent())
method call. This initializes the state of the ColorBloc
with the default state specified in the constructor of the ColorBloc
class.
Redux pattern
In Flutter, the Redux pattern can be implemented using the flutter_redux package. The package offers a Store class that allows the creation of a store for the application.
Furthermore, it includes a middleware feature that enables the interception and alteration of actions before they are processed by reducers.
Create an application that uses the Redux pattern
For us to better understand the Redux pattern, let’s create an app that also increases and decreases when a button is tapped but using the Redux package.
To use the Redux pattern in your Flutter application, you need to follow a few steps:
- Define the State: define the state of your application as a class that extends the Equatable class from the equatable package.
Equatable
is used to compare objects for equality, which is important for performance optimization.
import 'package:equatable/equatable.dart';
class CounterState extends Equatable {
final int count; CounterState({this.count = 0}); CounterState copyWith({int count}) {
return CounterState(count: count ?? this.count);
} @override
List<Object> get props => [count];
}
- Define the Actions: define the actions that can be dispatched to the store as classes. These actions should be immutable and contain all the information required to update the state.
class IncrementAction {}
class DecrementAction {}
- Define the Reducers: define reducers that take the current state and an action as input, and return a new state as output. Reducers should be pure functions, meaning that they should not have any side effects.
CounterState counterReducer(CounterState state, dynamic action) {
if (action is IncrementAction) {
return state.copyWith(count: state.count + 1);
} else if (action is DecrementAction) {
return state.copyWith(count: state.count - 1);
}
return state;
}
- Create the Store: create the store for your application using the Store class from the
flutter_redux package
. Pass the reducer function and initial state to the constructor of the Store class.
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:flutter/material.dart';
import 'package:redux_state_management/redux/actions.dart';
import 'redux/app_state.dart';
import 'redux/reducers.dart';
void main() {
final store = Store<CounterState>(
counterReducer,
initialState: CounterState(),
); runApp(MyApp(store));
}class MyApp extends StatelessWidget {
final Store<CounterState> store; MyApp(this.store); @override
Widget build(BuildContext context) {
return StoreProvider<CounterState>(
store: store,
child: MaterialApp(
title: 'Flutter Redux example',
home: CounterScreen(),
),
);
}
}
- Dispatch Actions: dispatch actions to the store using the dispatch method of the Store class.
In the following code, we dispatch the IncrementAction
and DecrementAction
actions when the respective buttons are pressed.
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Redux Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StoreConnector<CounterState, int>(
converter: (store) => store.state.count,
builder: (context, count) => Text(
count.toString(),
style: TextStyle(fontSize: 48),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StoreConnector<CounterState, VoidCallback>(
converter: (store) => () => store.dispatch(IncrementAction()),
builder: (context, callback) => ElevatedButton(
onPressed: callback,
child: Text('Increment'),
),
),
SizedBox(width: 16),
StoreConnector<CounterState, VoidCallback>(
converter: (store) => () => store.dispatch(DecrementAction()),
builder: (context, callback) => ElevatedButton(
onPressed: callback,
child: Text('Decrement'),
),
),
],
),
],
),
),
);
}
}
In the above code, we use the StoreConnector
widget from the flutter_redux
package to connect our widget to the store.
We used the converter
parameter to convert the state of the store to a value that can be used by the widget. In this case, we convert the count field of the CounterState
class to an integer.
The builder
parameter builds the widget using the converted value. We also used the two StoreConnector
widgets to connect the two buttons to the store.
The converter
parameter is used to create a callback
function that dispatches the respective action to the store when the button is pressed.
By following these steps, you can implement the Redux pattern for state management in your Flutter application. The Redux pattern is a great tool for managing the state of your application, especially for large and complex applications.
Conclusion
The use of BLoC and Redux depends on factors such as the size and complexity of your project, the team’s familiarity with the patterns, and personal preferences. Both BLoC and Redux have vibrant ecosystems with extensive community support and numerous packages available. By leveraging the power of these state management patterns, you can build robust and scalable Flutter applications with ease.
Thank you for reading Part 3 of this series on understanding state management in Flutter. We hope you found the information on BLoC and Redux useful .
If you’re interested in diving deeper into this topic, check out parts one and two.
Part 1: Understanding State Management in Flutter (Part 1)
In Part 1, we explore the stateful widget and what you should consider when choosing a state management technique. We discuss the lifecycle of a stateful widget, setState and we use the stateful widget to build a counter app . You can read it here: Understanding State Management in Flutter (Part 1) .
Part 2: # InheritedWidget and Provider State Management
In Part 2, we explore two popular state management approaches called Provider and InheritedWidget. We discuss their features, benefits, and we also use the state management techniques to create applications. You can read it here: Understanding State Management in Flutter (Part 2) .
Resources for Further Learning:
Redux Documentation: https://pub.dev/packages/flutter_redux
Flutter BLoC package: https://pub.dev/packages/flutter_bloc
Originally published at https://semaphoreci.com on July 27, 2023.