BLoC State Management in Flutter (Easy to Understand)

Hamza Asif
4 min readSep 29, 2024

--

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 where counterValue is 0.
  • 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 recent bloc 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 the Text widget whenever the CounterState changes.
  • Provides the current state which includes counterValue.

FloatingActionButton:

  • Increment Button: Dispatches IncrementEvent to the CounterBloc.
  • Decrement Button: Dispatches DecrementEvent to the CounterBloc.

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 reaches 5, 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 and BlocListener, 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.

--

--

Hamza Asif
Hamza Asif

Written by Hamza Asif

Udemy Instructor, Flutter Dev helping people Integrate ML & AI in Mobile Apps . Visit my courses https://www.udemy.com/user/e1c14fb5-1c9b-45ef-a479-bbc543e33254

No responses yet