BLoC State Management in Flutter (Easy to Understand)
BLoC (Business Logic Component) is a design pattern created by Google to help developers separate business logic from the presentation layer (UI) in Flutter applications. It leverages Streams to handle events and states, promoting a reactive programming paradigm.
Why Use BLoC?
- Separation of Concerns: Clearly separates UI from business logic, making the codebase more organized.
- Reusability: Business logic can be reused across different parts of the app or even in different projects.
- Testability: Isolated business logic is easier to unit test.
- Scalability: Suitable for large and complex applications due to its structured approach.
Project Structure
Here’s a suggested project structure when using BLoC:
lib/
│
├── blocs/
│ └── counter/
│ ├── counter_bloc.dart
│ ├── counter_event.dart
│ └── counter_state.dart
│
├── models/
│ └── (optional)
│
├── repositories/
│ └── (optional)
│
├── screens/
│ ├── home_screen.dart
│ └── (other screens)
│
├── widgets/
│ └── (reusable widgets)
│
└── main.dart
Understanding BLoC Components
To effectively implement BLoC, it’s essential to understand its core components: Events, States, and the BLoC itself.
Events
Events are actions or occurrences that happen in the app, typically initiated by user interactions or external inputs. They represent “what happened.”
States
States represent the current condition or status of the app at a particular point in time. They represent “what the UI should look like.”
BLoC
The BLoC listens for incoming events, processes them (usually involving business logic), and emits new states based on the events.
Example: Building a Simple Counter App
1. Adding Dependencies
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.2 # Check for the latest version on pub.dev
2. Creating BLoC Components
We’ll create a CounterBloc
along with its associated CounterEvent
and CounterState
.
a. Defining Events
Create a file named counter_event.dart
inside lib/blocs/counter/
:
// lib/blocs/counter/counter_event.dart
import 'package:equatable/equatable.dart';
abstract class CounterEvent extends Equatable {
const CounterEvent();
@override
List<Object> get props => [];
}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
Explanation:
- CounterEvent: An abstract class that extends
Equatable
for value comparison. - IncrementEvent & DecrementEvent: Concrete classes representing specific events.
Note: equatable
is used for value equality. Add it to your pubspec.yaml
if not already present:
dependencies:
equatable: ^2.0.5
b. Defining States
Create a file named counter_state.dart
inside lib/blocs/counter/
:
// lib/blocs/counter/counter_state.dart
import 'package:equatable/equatable.dart';
class CounterState extends Equatable {
final int counterValue;
const CounterState({required this.counterValue});
@override
List<Object> get props => [counterValue];
}
Explanation:
- CounterState: Holds the current value of the counter. Extends
Equatable
for easy state comparison.
c. Creating the BLoC
Create a file named counter_bloc.dart
inside lib/blocs/counter/
:
// lib/blocs/counter/counter_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_event.dart';
import 'counter_state.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(counterValue: 0)) {
on<IncrementEvent>((event, emit) {
emit(CounterState(counterValue: state.counterValue + 1));
});
on<DecrementEvent>((event, emit) {
emit(CounterState(counterValue: state.counterValue - 1));
});
}
}
Explanation:
- CounterBloc: Extends
Bloc<CounterEvent, CounterState>
. - Constructor: Initializes the BLoC with an initial
CounterState
wherecounterValue
is0
. - Event Handlers:
- IncrementEvent: Increments the
counterValue
by 1. - DecrementEvent: Decrements the
counterValue
by 1. - on<Event>: Registers event handlers using the
on
method, a recommended approach in recentbloc
versions.
3. Building the UI
We’ll create a HomeScreen
that interacts with CounterBloc
.
a. Providing the BLoC
In your main.dart
, provide the CounterBloc
to the widget tree using BlocProvider
.
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'blocs/counter/counter_bloc.dart';
import 'screens/home_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: MaterialApp(
title: 'Flutter BLoC Counter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomeScreen(),
),
);
}
}
- BlocProvider: Makes
CounterBloc
available to the widget subtree.
b. Consuming BLoC in Widgets
Create a file named home_screen.dart
inside lib/screens/
:
// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/counter/counter_bloc.dart';
import '../blocs/counter/counter_event.dart';
import '../blocs/counter/counter_state.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
// Access the CounterBloc instance
final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);
return Scaffold(
appBar: AppBar(
title: const Text('Flutter BLoC Counter'),
),
body: Center(
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text(
'Counter Value: ${state.counterValue}',
style: const TextStyle(fontSize: 24.0),
);
},
),
),
floatingActionButton: Padding(
padding: const EdgeInsets.only(left: 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'decrement',
onPressed: () => counterBloc.add(DecrementEvent()),
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
const SizedBox(width: 20),
FloatingActionButton(
heroTag: 'increment',
onPressed: () => counterBloc.add(IncrementEvent()),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
],
),
),
);
}
}
BlocBuilder:
- Listens to
CounterBloc
and rebuilds theText
widget whenever theCounterState
changes. - Provides the current
state
which includescounterValue
.
FloatingActionButton:
- Increment Button: Dispatches
IncrementEvent
to theCounterBloc
. - Decrement Button: Dispatches
DecrementEvent
to theCounterBloc
.
Running the App
flutter run
Advanced BLoC Features
1. BlocListener:
BlocListener
listens for changes in the BLoC's state and can perform actions in response, such as showing snackbars, dialogs, or navigation.
// Add import for ScaffoldMessenger
import 'package:flutter/material.dart';
// ... other imports
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);
return Scaffold(
appBar: AppBar(
title: const Text('Flutter BLoC Counter'),
),
body: BlocListener<CounterBloc, CounterState>(
listener: (context, state) {
if (state.counterValue == 5) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Counter reached 5!')),
);
}
},
child: Center(
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text(
'Counter Value: ${state.counterValue}',
style: const TextStyle(fontSize: 24.0),
);
},
),
),
),
floatingActionButton: // ... same as before
);
}
}
Explanation:
BlocListener:
- Monitors state changes.
- When
counterValue
reaches5
, it triggers a snackbar.
child:
- Contains the
BlocBuilder
to display the counter value.
2. BlocConsumer:
BlocConsumer: Combines both BlocBuilder
and BlocListener
, allowing you to rebuild UI and perform side effects in the same widget.
BlocConsumer<CounterBloc, CounterState>(
listener: (context, state) {
if (state.counterValue == 5) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Counter reached 5!')),
);
}
},
builder: (context, state) {
return Text(
'Counter Value: ${state.counterValue}',
style: const TextStyle(fontSize: 24.0),
);
},
)
BlocBuilder vs BlocListener vs BlocConsumer
- BlocBuilder: Rebuilds UI based on state changes.
- BlocListener: Performs side effects (e.g., navigation, showing dialogs) based on state changes.
- BlocConsumer: Combines both
BlocBuilder
andBlocListener
, allowing you to rebuild UI and perform side effects in the same widget.
Streams and Sinks
Under the hood, BLoC uses Streams to manage the flow of data and Sinks to handle event inputs. Understanding Streams can help you leverage BLoC more effectively, especially for complex state management scenarios.