Managing state effectively is crucial for creating a smooth user experience in any app. The Bloc pattern, one of Flutter’s most robust state management solutions, helps you achieve this with ease.
Bloc separates the UI from the business logic. Implementing Bloc ensures that the app is clean, scalable, testable, maintainable and reacts to changes efficiently and predictably.
We have briefly touched on it in last week’s article, but in this article we’ll break down the Bloc pattern step-by-step, with practical examples to help you manage state like a pro. We will also explore its simpler version Cubit and compare them.
What is Bloc/Cubit ?
The Bloc (Business Logic Component) pattern is a design pattern in Flutter that separates the UI from the business logic. It ensures that your app remains clean, maintainable, and scalable. Bloc uses Dart’s Streams to facilitate communication between the UI and the logic, creating a reactive architecture.
Before looking further into the Bloc pattern, let’s talk briefly talk about its smaller version: Cubit. Cubit is a lightweight state management solution that is part of the same ecosystem as Bloc, provided by the flutter_bloc
package. While Bloc uses Streams to handle events and states, Cubit simplifies the process by eliminating the need for events entirely.
Core Concepts
Bloc
At its core, Bloc processes the user actions/triggers (events) and emits corresponding visual representation (states) in response. This allows the app to handle complex state management while keeping the UI code straightforward. The flow usually goes as follows:
- Events: The user interacts with the UI, e.g. presses a button, generating an event
- Bloc: The event is sent to the Bloc, which Process the logic and decides what to do next.
- States: The Bloc emits a new state, which updates the UI accordingly.
Cubit
Cubit manages and emitting states directly, without relying on events to trigger changes. You can think of Cubit as a trimmed-down version of Bloc, suitable for scenarios where the business logic is straightforward and doesn’t require the complexity of event handling. Here is how it works:
- The UI interacts with the Cubit by calling functions (e.g., increment, decrement).
- The Cubit processes the logic within those functions.
- A new state is emitted directly, updating the UI.
Why Use Bloc?
Separation of Concerns:
The UI doesn’t directly handle business logic. Instead, the Bloc serves as the mediator, making your app easier to manage and test.
Reactive Programming:
Since Bloc is based on Streams, your app automatically reacts to state changes, ensuring a smooth and consistent user experience.
Scalability:
Bloc shines in larger apps where managing multiple states and interactions can become challenging.
Why Use Cubit
Simplicity:
With no events to define or map, Cubit is easier and faster to set up compared to BLoC.
Efficiency:
Fewer boilerplate code requirements mean a quicker development process.
Integration:
Cubit is part of the flutter_bloc
package, so you can seamlessly upgrade to BLoC if your app grows in complexity.
Setting Up Bloc/Cubit in a Flutter App
Open android studio and create a new project bloc_tutorial_app. We are going to build a counter app and use Bloc to manage state.
But before we start coding, let’s install the Bloc plugin to android studio; This will help us effectively create blocs and cubits.
Once the android studio window is open go to Setting > Plugins, search for Bloc and it’s the one by Felix Angelov. Click install and apply. Then click ok. (I already have it installed in the image below)
Dependencies
Add the flutter_bloc package to the pubspec.yaml file
dependencies:
flutter_bloc: ^8.0.0
Bloc
We are now ready to implement bloc. In main.dart remove every line of code except the following.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp();
}
}
Next create the file counter_screen.dart in the lib folder. In the file create CounterScreen as a StatelessWidget, since we will be managing state with Bloc
import 'package:flutter/material.dart';
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
Then in main.dart set the home property in MaterialApp to CounterScreen()
import 'package:bloc_tutorial_app/counter_screen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: CounterScreen(), // New
);
}
}
Now we will create our Bloc.
Right click on the lib folder and create a folder counter_bloc.
Right click on the counter_bloc folder, then click on New > Bloc Class. Name it counter and make sure the style is Basic. Click ok.
You should now have three files in counter_bloc: counter_bloc.dart, counter_event.dart, and counter_state.dart.
Events and States
Let’s create events for the buttons the user is going to press. We will have an button to increment and another to decrement. Go to the counter_event.dart file.
You should see that the CounterEvent class was generated.
part of 'counter_bloc.dart';
@immutable
sealed class CounterEvent {}
You may have noticed the keyword sealed and the @immutable.
A sealed class can only be extended by a predefined set of subclasses declared within the same file. This makes your code more type-safe when dealing with events and states.
@immutable is an annotation used to mark a class as immutable, meaning once an object of that class is created, its value cannot be changed. This makes the object “read-only” and any attempt to modify it will result in creating a new object with the updated values instead of altering the existing one.
Let’s create the classes IncrementEvent and DecrementEvent, which will extend CounterEvent
part of 'counter_bloc.dart';
@immutable
sealed class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
Checkout counter_state.dart. It should have the following generated code
part of 'counter_bloc.dart';
@immutable
sealed class CounterState {}
final class CounterInitial extends CounterState {}
Replace CounterInitial With CounterValue, which should have a variable counter to hold the data.
part of 'counter_bloc.dart';
@immutable
sealed class CounterState {}
class CounterValue extends CounterState {
final int counter;
CounterValue(this.counter);
@override
List<Object> get props => [counter]; // For state comparison
}
Now that we have defined events and states, we can go implement the bloc.
Implement the Bloc
The generated code from counter_bloc.dart should look like this
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
part 'counter_event.dart';
part 'counter_state.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitial()) {
on<CounterEvent>((event, emit) {
// TODO: implement event handler
});
}
}
- on<Event> handles specific events types
- emits() updates the state, trigger
Replace CounterInitial with CounterValue() and set the initial value of counter to 0.
Then remove the event handler for CounterEvent and implement event handlers for IncrementEvent and DecrementEvent. You should have something like this
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
part 'counter_event.dart';
part 'counter_state.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterValue(0)) {
on<IncrementEvent>((event, emit) {
final currentValue = (state as CounterValue).counter;
emit(CounterValue(currentValue + 1));
});
on<DecrementEvent>((event, emit) {
final currentValue = (state as CounterValue).counter;
emit(CounterValue(currentValue - 1));
});
}
}
Use Bloc in the UI
To ensure that CounterBloc is accessible anywhere in the app we should wrap the widget at the top of the widget tree, which is CounterScreen, with the BlocProvider widget.
BlocProvider
is a widget provided by the flutter_bloc
package that handles dependency injection for your Bloc or Cubit instances.
The code in main.dart should now look like this
import 'package:bloc_tutorial_app/counter_bloc/counter_bloc.dart';
import 'package:bloc_tutorial_app/counter_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (context) => CounterBloc(),
child: CounterScreen(),
), // New
);
}
}
In the CounterScreen class let’s create the UI. In a Scaffold widget we will add two FloatingActionButton widgets to increment and decrement the value of
import 'package:flutter/material.dart';
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter BLoC Counter')),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () {},
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
const SizedBox(width: 10),
FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
],
),
);
}
}
For each FloatingActionButton in the onPressed function add events to increment or decrement the counter.
import 'package:bloc_tutorial_app/counter_bloc/counter_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter BLoC Counter')),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(DecrementEvent()), // New
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
const SizedBox(width: 10),
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(IncrementEvent()), // New
tooltip: 'Increment',
child: const Icon(Icons.add),
),
],
),
);
}
}
in the body, we can display the counter value in a Text widget. In order to rebuild the Text widget when the state changes we will wrap it with The BlocBuilder widget, provided by the flutter_bloc package.
import 'package:bloc_tutorial_app/counter_bloc/counter_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter BLoC Counter')),
// New
body: Center(
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
final counterValue = (state as CounterValue).counter;
return Text(
'Counter Value: $counterValue',
style: TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(DecrementEvent()),
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
const SizedBox(width: 10),
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(IncrementEvent()),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
],
),
);
}
}
Cubit
As we did for Bloc, create a folder counter_cubit and inside it, instead of Bloc Class, click on Cubit Class to generate the files counter_cubit.dart and counter_state.dart
States
The code for the state file for Cubit should be the same as the state file for Bloc.
@immutable
sealed class CounterState {}
class CounterValue extends CounterState {
final int counter;
CounterValue(this.counter);
@override
List<Object> get props => [counter]; // For state comparison
}
Implement the Cubit
Since cubit doesn’t use events we will create functions increment() and decrement()
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
part 'counter_state.dart';
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(CounterValue(0));
void increment() {
final currentValue = (state as CounterValue).counter;
emit(CounterValue(currentValue + 1));
}
void decrement() {
final currentValue = (state as CounterValue).counter;
emit(CounterValue(currentValue - 1));
}
}
UI with Cubit
In main.dart replace CounterBloc with CounterCubit.
import 'package:bloc_tutorial_app/counter_cubit/counter_cubit.dart';
import 'package:bloc_tutorial_app/counter_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (context) => CounterCubit(), // New
child: const CounterScreen(),
),
);
}
}
In CounterScreen do the same thing and replace CounterBloc with CounterCubit
Since we’re using Cubit, instead of adding events we just call the functions we created for each button
import 'package:bloc_tutorial_app/counter_cubit/counter_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter BLoC Counter')),
body: Center(
child: BlocBuilder<CounterCubit , CounterState>( // Changed to CounterCubit
builder: (context, state) {
final counterValue = (state as CounterValue).counter;
return Text(
'Counter Value: $counterValue',
style: const TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => context.read<CounterCubit>().decrement(), // New. Changed to CounterCubit
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
const SizedBox(width: 10),
FloatingActionButton(
onPressed: () => context.read<CounterCubit>().increment(), // New. Changed to CounterCubit
tooltip: 'Increment',
child: const Icon(Icons.add),
),
],
),
);
}
}
Conclusion
The Bloc pattern is a powerful and scalable solution for managing state in Flutter apps. By separating business logic from the UI and leveraging Streams, Bloc ensures your app is maintainable, testable, and reactive.
In this post, we covered the basics of Bloc, how it compares to Cubit, and walked through a practical implementation. Whether you’re building a simple app or a complex project, Bloc provides the structure you need for clean and efficient state management.
Experiment with Bloc in your own projects and see the difference it can make. Feel free to share your thoughts or ask questions in the comments