DEV Community

Cover image for Flutter BLoC: Building a Feature with Events, States, and Cubit
Yadnesh Teli
Yadnesh Teli

Posted on AI-assisted

Flutter BLoC: Building a Feature with Events, States, and Cubit

Series 4 — Part 1 of 1

Flutter BLoC: Building a Feature with Events, States, and Cubit

A standalone deep-dive on the BLoC pattern in Flutter

After GetX, my next exploration was Flutter BLoC — the pattern that treats UI as a function of state, and models every user action as an explicit event. BLoC is more verbose than either Riverpod or GetX, but it is also the most explicit about intent.

I spent two days reading the official BLoC documentation, watching walkthroughs, and building a basic implementation with MVVM architecture.

The BLoC mental model

BLoC stands for Business Logic Component. The pattern works like this: the UI emits Events, the BLoC processes them and emits new States, and the UI rebuilds based on the current State. Nothing is hidden. Every transition is traceable.

The flutter_bloc package gives you two primitives:

  • Cubit: The simpler primitive — it holds state and exposes methods to update it, without an explicit event hierarchy.
  • Bloc: The full version — you define an event class hierarchy, and an on<Event> handler that emits new states.

Implementing a feature with BLoC

I built a product listing feature using Bloc. The events were LoadProducts and RefreshProducts. The states were ProductsInitial, ProductsLoading, ProductsLoaded, and ProductsError.

class ProductBloc extends Bloc<ProductEvent, ProductState> {
  final ProductRepository _repo;

  ProductBloc(this._repo) : super(ProductsInitial()) {
    on<LoadProducts>((event, emit) async {
      emit(ProductsLoading());
      try {
        final products = await _repo.fetchProducts();
        emit(ProductsLoaded(products));
      } catch (e) {
        emit(ProductsError(e.toString()));
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The UI uses BlocBuilder to rebuild on state changes:

BlocBuilder<ProductBloc, ProductState>(
  builder: (context, state) {
    if (state is ProductsLoading) return const CircularProgressIndicator();
    if (state is ProductsLoaded) return ProductList(items: state.products);
    if (state is ProductsError) return Text('Error: ${state.message}');
    return const SizedBox.shrink();
  },
)
Enter fullscreen mode Exit fullscreen mode

The advantage is total clarity — you can read the bloc class and understand every possible state transition without reading any UI code.

BLoC with MVVM

I structured the app so that BLoCs acted as ViewModels and repositories remained the data layer. BLoC maps cleanly to MVVM — the Bloc is the ViewModel, the state is what the View renders, and events are the actions the View sends.

Fixing bugs in the implementation

  • Provider Scope: I hit a routing issue where the BlocProvider was not available in the subtree that needed it. The fix was to move the BlocProvider higher in the widget tree, above the route that consumed it.
  • Rebuild detection with Equatable: I had a state management issue where emitting a new state inside an event handler was not triggering a rebuild — this was caused by using the same state instance without Equatable overriding equality correctly.

What I learned

  • Explicit transitions: BLoC is the most explicit of the three patterns — every state and every transition is named.
  • Cubit as an on-ramp: Cubit is a simpler starting point. Upgrade to full Bloc when you need event history or complex transitions.
  • Widget tree placement: BlocProvider must be placed above the widget that consumes the Bloc in the tree.
  • Equatable is essential: Equatable is required for BlocBuilder to detect state changes correctly.
  • Team collaboration: BLoC is excellent for teams — the event/state contracts are self-documenting.

Top comments (0)