Mastering Flutter BLoC: Best Practices for State, Event, and BLoC Folders
Teqani Blogs
Writer at Teqani
Managing state in Flutter apps becomes significantly more manageable and scalable when leveraging the BLoC (Business Logic Component) pattern. A clean and efficient way to structure your BLoC code is by organizing it into three distinct folders: event, state, and bloc. This article demonstrates how to implement this folder structure and provides a simple counter example.
Why Organize with State, Event, and BLoC Folders?
- event: Contains all possible actions or events (like button presses).
- state: Holds the different states your UI can display.
- bloc: Contains the logic for handling events and producing new states.
This structure ensures your project remains modular, readable, and easily scalable as your app grows.
Folder Structure
Here’s how your lib
folder might look:
lib/
└── bloc/
└── counter_bloc.dart
└── event/
└── counter_event.dart
└── state/
└── counter_state.dart
Step 1: Define Events
Inside event/counter_event.dart
:
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
Step 2: Define State
Inside state/counter_state.dart
:
class CounterState {
final int counterValue;
CounterState(this.counterValue);
}
Step 3: Create the BLoC
Inside bloc/counter_bloc.dart
:
import 'package:flutter_bloc/flutter_bloc.dart';
import '../event/counter_event.dart';
import '../state/counter_state.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)) {
on<Increment>((event, emit) => emit(CounterState(state.counterValue + 1)));
on<Decrement>((event, emit) => emit(CounterState(state.counterValue - 1)));
}
}
Step 4: Connect BLoC to the UI
Here’s a minimal main.dart
to tie it all together:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/counter_bloc.dart';
import 'event/counter_event.dart';
import 'state/counter_state.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (_) => CounterBloc(),
child: CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = BlocProvider.of<CounterBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Flutter BLoC Counter')),
body: Center(
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) =>
Text('${state.counterValue}', style: TextStyle(fontSize: 48)),
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: "decrement",
onPressed: () => bloc.add(Decrement()),
child: Icon(Icons.remove),
),
SizedBox(width: 16),
FloatingActionButton(
heroTag: "increment",
onPressed: () => bloc.add(Increment()),
child: Icon(Icons.add),
),
],
),
);
}
}
Core BLoC Keywords & Classes
These are the fundamental classes provided by the bloc and flutter_bloc packages:
- Bloc 📌 The core class for business logic
Requires Event (input) & State (output). Uses mapEventToState for reactive state changes.
- Cubit ⚡ A lightweight alternative to Bloc
Uses functions (e.g., emit()) instead of events. Simpler syntax, ideal for basic state management.
- BlocProvider 🌐 Dependency Injection for Bloc/Cubit
A Flutter widget that provides a Bloc/Cubit to its children. Ensures a single instance is available down the widget tree.
- BlocBuilder 🔄 Reactive UI Updates
Rebuilds only the necessary widgets when state changes. Optimized for performance with buildWhen.
- BlocListener 🎧 Side Effects Handler
Calls callbacks (e.g., navigation, dialogs) on state changes. Does not rebuild UI (ideal for one-time actions).
- BlocConsumer 🎭 Best of Both Worlds
Combines BlocBuilder (UI updates) + BlocListener (side effects). Reduces boilerplate for common use cases.
- MultiBlocProvider 🧩 Multi-Bloc Dependency Injection
Provides multiple Blocs to a subtree in a clean way. Avoids nested BlocProvider widgets.
- RepositoryProvider 🗃️ Clean Architecture Companion
Supplies a repository (data layer) to the widget tree. Works seamlessly Bloc for the separation of concerns.
Event & State Management
- Event ⚡ Triggers state changes in your Bloc
Represents user actions or app triggers (e.g., LoginButtonPressed, FetchUserData). Events are added to the Bloc via add(event).
- State 📊 Represents the app’s condition at any moment
Examples:
- LoginInitial → No action yet.
- LoginLoading → API call in progress.
- LoginSuccess → User logged in.
- LoginFailure → Error occurred.
The UI reacts to state changes.
- emit() 🚀 Sends a new state from Bloc/Cubit
Used inside Bloc/Cubit to update the state.
// Example (Cubit): void login() { emit(LoginLoading()); // ... API call ... emit(LoginSuccess(user)); }
Example Snippet Using Key BLoC Keywords
// 1. Define Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
// 2. Define States
class CounterState extends Equatable {
final int count;
const CounterState(this.count);
@override
List<Object> get props => [count];
}
// 3. Create the Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(0)) {
on<Increment>((event, emit) => emit(CounterState(state.count + 1)));
on<Decrement>((event, emit) => emit(CounterState(state.count - 1)));
}
}
// 4. Integrate with Flutter UI
BlocProvider(
create: (context) => CounterBloc(),
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text('Count: ${state.count}');
},
),
);
Conclusion
Organizing your Flutter BLoC code into state, event, and bloc Folders helps keep your codebase clean and scalable. This approach is easy to follow and works great for both small and large projects.
All blogs are certified by our company and reviewed by our specialists
Issue Number: #e90babef-4c03-4f85-b4a9-6918c4919c72