How to Use BLoC State Management in Flutter

State management is the part of a Flutter app that quietly decides whether the codebase stays pleasant to work in or turns into a tangle. BLoC. short for Business Logic Component. is one of the most widely used patterns for keeping that under control. The idea is simple: pull the business logic out of your widgets and let the two sides talk through streams. Here's what that means in practice and how to wire it up.
What BLoC actually is
BLoC is an architectural pattern that separates the presentation layer (your UI) from the business logic layer. You create a dedicated class. the BLoC. that owns your app's state and exposes it to the UI through streams. The widget sends events in; the BLoC emits new states out. Because the two never reach directly into each other, your interface stays thin and your logic stays testable.

Why teams reach for it
There are a few concrete payoffs once the pattern is in place:
- Separation of concerns. With the presentation layer split from the business logic, the code stays organized and far easier to maintain as the app grows.
- Reusability. Since the logic lives outside the widgets, you can reuse the same BLoC across multiple screens and components instead of duplicating it.
- Testability. You can test the business logic on its own, without spinning up the UI. which makes automated tests both faster to write and more reliable.
Putting BLoC to work
A full example is the clearest way to see it. We'll build a small counter app. the Flutter “hello world” of state management. in three steps.
Step 1: Define the events and the BLoC
Start with the events. Modelling them as a sealed class lets the compiler tell you when a handler is missing, which is the main reason it is preferred over a plain enum in current Dart.
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
final class CounterDecrementPressed extends CounterEvent {}Then the BLoC itself. It extends Bloc from the flutter_bloc package, passes its initial state to super, and registers one handler per event with on<Event>. Each handler receives the event and an emit function, and calling emit pushes a new state to the UI. For the counter, the state is just an integer.
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
on<CounterDecrementPressed>((event, emit) => emit(state - 1));
}
}If you have read an older BLoC tutorial you may recognisemapEventToState, a single method that returned a stream of states. It was removed in bloc 8 and does not compile against current releases. Theon<Event>+emitform above replaces it.
Step 2: Build your UI
Next, the screen. Notice that the widget holds no counter value of its own. It reads the current count from the BLoC and dispatches a CounterIncrementPressed or CounterDecrementPressed event when the buttons are tapped.
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Center(
child: Text('$count', style: const TextStyle(fontSize: 24)),
);
},
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () =>
context.read<CounterBloc>().add(CounterIncrementPressed()),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
const SizedBox(height: 10),
FloatingActionButton(
onPressed: () =>
context.read<CounterBloc>().add(CounterDecrementPressed()),
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
],
),
);
}
}Step 3: Connect the UI to the BLoC
Finally, hand the BLoC down to the UI. The BlocBuilder widget from flutter_bloc rebuilds whenever CounterBloc's state changes, and context.read() gives the buttons a handle to dispatch events. Wrapping the page in a BlocProvider is what makes that BLoC available to everything below it.
BlocProvider(
create: (_) => CounterBloc(),
child: const CounterPage(),
)The pattern always comes back to the same shape: the UI adds events, a registered handler emits new states, and the widgets rebuild. Once that clicks, scaling from a counter to a real app is the same idea repeated.
When a Cubit is the better choice
BLoC is not the only option inside the same package. A Cubit drops the event layer entirely and exposes plain methods that call emit, which is less ceremony for state that changes in obvious ways. Reach for a full BLoC when you want an auditable record of what happened rather than only what changed, which matters for debugging, analytics and replaying user journeys.
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}BLoC isn't the only way to manage state in Flutter, but it earns its place on teams that value clean boundaries and solid test coverage. If you're weighing it for a project. or want a team that's shipped plenty of Flutter apps to do it for you. tell us what you're building, or take a look at the apps we've shipped.
Thinking about building this?
Appluex designs and ships production mobile & web apps. Including AI features. Let's talk.