← Back to list

Stop Writing BLoCs

Most app state isn’t a state machine — and pretending it is has produced a generation of Flutter codebases drowning in events, states, and…

Mouaz M. Al-Shahmeh · 2026-05-25 07:45 · 9 claps · 14.3 min read
#flutter #bloc #state-management
Open on Medium ↗
Wiki topics: BIZ · Business Strategy 📱 · Mobile Development

Stop Writing BLoCs

Most app state isn’t a state machine — and pretending it is has produced a generation of Flutter codebases drowning in events, states, and transitions for problems that didn’t need any of them. A senior engineer’s case for restraint in 2026.

I want to make this clear up front so the rest of the article can be read in good faith: BLoC is a good pattern. Felix Angelov and the maintainers who’ve built it over the years have produced one of the most thoughtful, well-documented, and battle-tested state management libraries in the Flutter ecosystem. The library is excellent. The community is serious. The patterns are well-considered.

This article is not about whether BLoC is good. It’s about whether BLoC is appropriate for the way most teams are using it in 2026. My argument, which I’ll defend in detail across this piece, is that the answer is “no, and the cost of that misuse has compounded into one of the most expensive ongoing taxes on Flutter productivity in our ecosystem.”

I’ve been thinking about this for years. I’ve watched teams adopt BLoC by default, scale it to 80, 150, 300 BLoCs in a codebase, and then spend their senior engineering bandwidth on the structural overhead of maintaining a state-machine pattern for problems that weren’t state machines. I’ve watched teams quietly migrate away from BLoC after years of investment, and I’ve watched teams successfully use BLoC for the narrow set of problems where it’s the right tool. The pattern, across both successes and failures, is consistent enough that I think it’s time to articulate the case for restraint publicly.

If you maintain a BLoC-heavy codebase, this article is going to push at some choices you’ve made. I want to engage with you in good faith. The argument below is technical, not personal. If you finish reading and disagree, I genuinely want to hear it — the comments are the best place to have the conversation in a way that sharpens both of our thinking. But please read the whole argument before responding to the title.

Let’s go.

What BLoC Actually Is

Before any argument about whether BLoC fits a given problem, we need to be precise about what BLoC actually is — because the casual conversation often conflates BLoC the library with BLoC the pattern, and the distinction matters.

BLoC the pattern is a specific approach to state management with three structural commitments:

  1. Events as the only way to trigger state changes. External code does not call methods on the BLoC to change state. It sends events. The BLoC processes events and produces states.
  2. States as discrete, enumerable values. Each state is a typed object. Transitions between states are explicit and visible in the code.
  3. A clear separation between event input, transition logic, and state output. This is the unidirectional flow that makes the pattern testable, predictable, and reasonable to debug.

These three commitments together describe a specific data structure: a finite-state machine. Not metaphorically. Literally. A finite-state machine in computer science is a system with a finite set of states, a finite set of inputs (events), and a transition function that maps (state, input) pairs to new states. That’s exactly what BLoC describes.

This is not a criticism. Finite-state machines are a genuinely powerful abstraction, and there are categories of state that are best understood, implemented, and debugged as state machines. The mistake isn’t using BLoC. The mistake is using BLoC for state that isn’t a state machine — which, in most apps, is most of the state.

What Most App State Actually Is

Walk through a typical Flutter app and inventory the categories of state you actually have. In the apps I’ve worked on across the past several years — fintech, food delivery, healthcare, B2B tooling — the categories break down roughly like this:

Category 1: Async data from a remote source. A list of products. A user profile. A feed of messages. The latest currency exchange rate. These are not state machines. They are values that exist in one of four states — idle, loading, loaded, error — and the transitions between them are entirely determined by the async operation, not by user events. This is what AsyncValue in Riverpod, or FutureBuilder in vanilla Flutter, was designed for. Modeling these as BLoCs adds ceremony without adding clarity.

Category 2: Derived state. The number of items in a cart. The total of a list of prices. Whether a form is valid. The filtered view of a list based on a search query. Derived state is computed from other state. It doesn’t need events; it needs a derivation function and a reactivity system that triggers recomputation when dependencies change. Riverpod’s Provider and computed Signals were designed precisely for this. Modeling derived state as a BLoC requires you to manually fire events to recompute things that should recompute themselves.

Category 3: Local UI state. The currently-selected tab. Whether a dropdown is open. The scroll position of a list. Whether a checkbox is checked. This state is local to one widget, doesn’t need to be shared across screens, doesn’t need to be persisted, and doesn’t have any complex transitions. It belongs in setState or a small ValueNotifier ninety percent of the time. Modeling these as BLoCs is over-engineering at the syntactic level.

Category 4: Form state. The current values of fields in a form. Validation errors. Submission state. Forms have a specific shape — many fields, each with their own value and validation, plus an overall submission state. There are excellent form libraries that handle this directly. Modeling forms as BLoCs typically requires either firing events for every keystroke (which is awkward) or batching event firing (which makes validation feel laggy).

Category 5: Genuinely event-driven, multi-state flows. Multi-step checkout. BLE device pairing. Multi-factor authentication flows. Payment processing with 3D Secure redirects. Video upload pipelines with retries. Onboarding wizards with conditional branches. This is the category where BLoC genuinely fits. These are state machines in the formal sense — finite enumerable states, explicit transitions on events, no ambiguity about what happens when. For category 5 state, BLoC is the right tool.

The honest math: in a typical Flutter app, categories 1 through 4 are 90–95% of the state, and category 5 is 5–10%. When teams default to BLoC for everything, they’re applying a tool designed for the 5–10% to the entire codebase. The cost of that mismatch is what this article is about.

The Tax

What does “BLoC for everything” actually cost? I want to be specific because the abstract argument doesn’t land without examples.

Code-volume cost

A simple feature — “load a list of products and display them” — in a Riverpod or Signals-based codebase requires:

// Riverpod 3
@riverpod
Future<List<Product>> products(Ref ref) async {
  return ref.read(productRepositoryProvider).list();
}

// Usage in widget
final productsAsync = ref.watch(productsProvider);
return productsAsync.when(
  data: (products) => ProductList(products: products),
  loading: () => const CircularProgressIndicator(),
  error: (e, _) => ErrorView(error: e),
);

That’s the entire implementation. About 15 lines including the widget consumption.

The equivalent in BLoC requires:

// product_event.dart
sealed class ProductEvent {}
class LoadProducts extends ProductEvent {}
class RefreshProducts extends ProductEvent {}

// product_state.dart
sealed class ProductState {}
class ProductInitial extends ProductState {}
class ProductLoading extends ProductState {}
class ProductLoaded extends ProductState {
  final List<Product> products;
  ProductLoaded(this.products);
}
class ProductError extends ProductState {
  final String message;
  ProductError(this.message);
}
// product_bloc.dart
class ProductBloc extends Bloc<ProductEvent, ProductState> {
  final ProductRepository repository;

  ProductBloc(this.repository) : super(ProductInitial()) {
    on<LoadProducts>(_onLoadProducts);
    on<RefreshProducts>(_onRefreshProducts);
  }

  Future<void> _onLoadProducts(LoadProducts event, Emitter<ProductState> emit) async {
    emit(ProductLoading());
    try {
      final products = await repository.list();
      emit(ProductLoaded(products));
    } catch (e) {
      emit(ProductError(e.toString()));
    }
  }

  Future<void> _onRefreshProducts(RefreshProducts event, Emitter<ProductState> emit) async {
    // Similar implementation, slightly different to preserve UI on refresh
  }
}
// Widget usage
BlocBuilder<ProductBloc, ProductState>(
  builder: (context, state) {
    return switch (state) {
      ProductInitial() || ProductLoading() => const CircularProgressIndicator(),
      ProductLoaded(:final products) => ProductList(products: products),
      ProductError(:final message) => ErrorView(message: message),
    };
  },
);

We’re at roughly 50–60 lines for the equivalent implementation. The BLoC version is more explicit about state transitions, but the cost is a 4x volume increase for a feature that didn’t have state-machine semantics to begin with.

Multiplied across hundreds of features in a real codebase, this becomes one of the largest accidental costs in modern Flutter development. The lines you don’t write are the lines you don’t have to maintain, debug, refactor, or test.

Cognitive cost

The volume cost compounds into cognitive cost. When a new engineer joins a BLoC-heavy codebase, they have to internalize:

  • Where event classes live for each feature
  • Where state classes live for each feature
  • The convention for naming events and states
  • The mapping between events and methods on the BLoC
  • The pattern for handling errors within event handlers
  • The pattern for testing each layer separately

In a Riverpod-based codebase, the equivalent learning is “providers are functions that return values, mostly async, mostly auto-disposing.” That’s it. The simplicity isn’t a feature of Riverpod specifically; it’s a feature of using a tool that matches the shape of the problem.

Maintenance cost

When the data model evolves — say, a Product gains a new field — a Riverpod implementation updates exactly one place: the data model. The provider re-runs, the widget re-renders, done.

The same change in a BLoC implementation may require updates in the event class (if events carry the data), the state class (to expose it), the BLoC (if transitions reference it), and the widgets (to consume it). Each change is small. The aggregate is real, especially when multiplied across hundreds of small evolutions over a codebase’s lifetime.

Testing cost

BLoC testing is well-supported via bloc_test, and it's genuinely good when you're testing actual state machines. But for a simple data-fetch feature, you're now writing tests like:

blocTest<ProductBloc, ProductState>(
  'emits [Loading, Loaded] when LoadProducts is added and repository succeeds',
  build: () {
    when(() => repo.list()).thenAnswer((_) async => fakeProducts);
    return ProductBloc(repo);
  },
  act: (bloc) => bloc.add(LoadProducts()),
  expect: () => [ProductLoading(), ProductLoaded(fakeProducts)],
);

The equivalent Riverpod test reads more like a direct data assertion:

test('products provider returns list from repository', () async {
  final container = ProviderContainer(overrides: [
    productRepositoryProvider.overrideWithValue(FakeRepo(products: fakeProducts)),
  ]);

  final products = await container.read(productsProvider.future);
  expect(products, fakeProducts);
});

Both tests verify the same behavior. The BLoC test verifies the state-machine transitions; the Riverpod test verifies the data flow. For features that aren’t state machines, the state-machine assertions add ceremony without adding signal.

The 2026 Alternative Stack

What should you use instead of BLoC for the 90–95% of state that isn’t a state machine? The 2026 answer is a layered stack, not a single library.

For data and derived state: Riverpod 3. The @riverpod annotation, code generation, AsyncValue pattern matching, autoDispose by default, computed providers, and family providers cover essentially every async data fetch, derived value, and shared application state need in a typical app. Riverpod's mental model maps directly onto the shape of these problems: a provider is a function that returns a value, and values flow through the graph automatically.

For wide-reactive-surface scenarios: Signals 6. Large forms, dashboards with many independently-updating fields, real-time data with high update frequency, collaborative editing surfaces — these benefit from fine-grained reactivity where individual widget rebuilds are scoped to specific values rather than entire screens. Signals shines here in ways Riverpod (with its coarser provider-level reactivity) doesn’t.

For local UI state: setState and ValueNotifier. Don't over-engineer. A checkbox doesn't need a state management library. A dropdown doesn't need a provider. The Flutter framework's built-in state primitives are excellent for ephemeral, local, single-widget state.

For genuine state machines: BLoC. This is the part the article isn’t trying to take away. Multi-step checkouts, BLE pairing, MFA flows, payment processing — these are exactly what BLoC was designed for, and using BLoC for them is the right call. The explicit events, explicit states, explicit transitions, and excellent testing support are all justified by the state-machine semantics of the problem.

This layered stack handles essentially every state management need in a typical Flutter app while paying the BLoC tax only where it’s earned. A senior engineer’s bias should be toward the lightest tool that adequately models the problem. BLoC is rarely the lightest. It’s appropriate when it’s earned, overkill when it isn’t.

When BLoC Is Genuinely Right

I want to be specific about when I do reach for BLoC, because the article would be dishonest without this section.

Payment processing flows. Most payment integrations have explicit states: idle, collecting payment method, tokenizing card, requires-3DS-authentication, authorizing, succeeded, failed-recoverable, failed-permanent. These states have explicit transitions on events (user submits, 3DS callback received, gateway responds, user cancels). BLoC’s structure maps directly onto this. Modeling payment processing as a simple async function would lose the ability to handle the 3DS redirect mid-flow cleanly.

Authentication and session state. Logged-out, logging-in, MFA-pending, MFA-verifying, logged-in, refreshing-tokens, logged-out-due-to-expiry. Multiple states, multiple events, explicit transitions, requires deterministic behavior under concurrent operations. BLoC fits.

Multi-step wizards with conditional branching. Onboarding flows where step 4 depends on choices in step 2, step 5 is sometimes skipped based on user attributes, and the user can navigate backwards. The branching logic benefits from explicit state representation.

Hardware integration with finite protocols. BLE pairing has explicit phases: scanning, found, connecting, discovering services, pairing, paired, disconnected. Each transition is triggered by an event from the OS. BLoC models this exactly.

Long-running upload or download pipelines. Video upload with retry, chunked file upload with pause/resume, sync operations with conflict resolution. State-machine semantics with explicit recoverable and unrecoverable states.

If your feature looks like one of these — if you can draw a state diagram on a whiteboard and have everyone in the room agree on what the diagram represents — BLoC is probably the right call. If your feature is “fetch some data and display it” or “compute a value from other values” or “manage form field state,” it isn’t.

The discipline isn’t “never use BLoC.” The discipline is “use BLoC where it’s earned by the shape of the problem, and use lighter tools everywhere else.”

Why Teams Default to BLoC Anyway

If the layered stack above is so clearly better for most state, why do so many teams default to BLoC for everything? I’ve spent enough time around teams making this choice to identify the pattern, and it’s worth naming explicitly.

1. BLoC is presented as a complete solution. The Bloc library’s documentation is comprehensive, the patterns are well-documented, and teams looking for “the way to do state management” find a complete answer. Riverpod is also a complete answer, but its documentation is less prescriptive — it tells you the primitives and leaves the patterns to you. For teams looking for a recipe to follow, BLoC’s prescriptive structure feels safer.

2. The state-machine model feels more “professional.” There’s a cultural perception in some parts of the Flutter community that explicit state management with events and states is the “serious” approach, and that lighter tools are for prototypes. This perception isn’t based on engineering rigor; it’s based on the visual weight of BLoC code, which feels more substantial because it has more files. Senior engineers should resist this bias. The right tool isn’t the heaviest one; it’s the one whose shape matches the problem’s shape.

3. Once you’ve adopted BLoC, the cost of mixing approaches feels high. A codebase with 100 BLoCs and the team’s BLoC mental model can’t easily introduce Riverpod for a few features without confusion. The path of least resistance is to keep writing BLoCs. The migration cost is real and creates lock-in.

4. Job market signals reinforce the pattern. Some hiring managers list “experience with BLoC” as a requirement, which encourages engineers to gain BLoC experience even if it isn’t the right tool for the role they’re working in. The job market shapes the technology choices, which shapes the job market, in a recursive loop that’s hard to break individually.

5. BLoC handles the genuinely hard cases well. When teams use BLoC for the 5–10% of state where it’s right, they have a good experience. They then generalize from those successes to all of their state management, missing that the lighter cases would have been even easier with lighter tools.

None of these reasons are crazy. They explain why thoughtful engineers default to BLoC. They also explain why, after years of this default, the ecosystem has accumulated significant unnecessary complexity that better tooling could address.

A Migration Path (If You Want One)

If you maintain a BLoC-heavy codebase and find this argument compelling, you’re probably wondering whether migration is feasible. My honest answer: don’t migrate wholesale. Migrate incrementally, starting with the cases where BLoC is most clearly overkill.

A practical sequence:

Step 1: Identify the BLoCs that are really just async data fetches. Look for BLoCs with three events (Load, Refresh, Reset) and four states (Initial, Loading, Loaded, Error). These are the clearest mismatches. Migrate them to Riverpod’s AsyncNotifier or simple FutureProvider one at a time, validating that behavior is preserved.

Step 2: Identify BLoCs that are really just derived state. Look for BLoCs whose only purpose is to combine values from other BLoCs and recompute when those values change. Migrate to Riverpod’s computed providers.

Step 3: Leave the state-machine BLoCs alone. Payment flows, auth state, onboarding wizards — if BLoC fits the shape, don’t migrate. The work isn’t worth the consistency.

Step 4: Establish a team convention. New features default to Riverpod (or Signals where appropriate) unless they have state-machine semantics, in which case BLoC is fine. The convention prevents the codebase from accumulating new mismatches even while the old ones are being addressed.

Step 5: Accept that some BLoCs will live forever. Migration is expensive. Not every BLoC needs to be migrated. The goal isn’t a BLoC-free codebase; the goal is a codebase where each tool is doing the job it’s best at.

This is the migration path I’ve watched work on multiple production codebases. The teams that try to migrate everything at once burn out and revert. The teams that migrate incrementally end up with healthier codebases over twelve to eighteen months.

What This Means for the Flutter Ecosystem

A broader observation, because this article exists in a context.

The Flutter team’s official architecture guide, published in early 2026, notably uses ChangeNotifier and Provider for its examples — not BLoC. This was a deliberate choice by the framework team, and it's a meaningful signal. The framework team's read of the ecosystem is that BLoC is appropriate for some applications but isn't the universal recommendation. The official documentation now reflects that.

I think this signal matters. The Flutter community has been having an unresolved conversation about state management for years, with strong opinions on multiple sides. The framework team has now positioned itself with a recommendation that doesn’t center BLoC. That doesn’t mean BLoC is wrong; it means the official recommendation has moved toward lighter primitives, and the community will gradually follow.

The senior engineers in the ecosystem who have been quietly using Riverpod (or Signals, or layered combinations) have been ahead of this curve for years. The teams still defaulting to BLoC for everything are not wrong, exactly — they’re working with patterns that were prevalent in 2021–2023 and haven’t yet fully updated to the 2026 consensus.

The good news is that the consensus is genuinely converging. The 2026 stack — lightweight defaults plus BLoC for state machines — is becoming the senior practitioner’s position across the community. The job market will follow the practitioners. The hiring requirements will catch up. The teaching content will catch up. The transition is happening, slowly.

This article is part of the transition.

A Closing Word

I want to close with respect, because the genre of “stop using X” articles often slides into condescension toward the people who built and use X, and that’s not what I’m trying to do.

The Bloc library is excellent. The maintainers have produced one of the most thoughtful, well-tested, well-documented state management libraries in the Flutter ecosystem. The pattern is appropriate for a specific category of problems, and for that category, BLoC is genuinely the right tool. Teams that use BLoC well are not making a mistake. They are making a choice that fits their problems, their team’s expertise, and their codebase’s history.

What this article is arguing against isn’t BLoC. It’s the default of BLoC for problems that don’t need a state machine. The mismatch between tool and problem produces codebases that are harder to maintain than they need to be, that take longer to onboard junior engineers into, that accumulate complexity at a rate that compounds over years. That cost is real, and a senior engineer’s responsibility is to recognize when a default has stopped serving the codebase and to advocate for change.

Use BLoC where it fits. Use lighter tools where they fit better. Resist the cultural pressure to over-engineer state management. Trust the framework’s lighter primitives more than you may have been taught to. The compound interest on these decisions, over years, is the difference between a codebase that scales and one that doesn’t.

The Flutter ecosystem in 2026 is mature enough to support a layered approach to state management. We should use that maturity. The senior engineers shaping how this work gets done in the next five years will define how it gets done for the rest of the decade. Be one of them. And if you disagree with this article — really disagree, with specifics — please push back in the comments. The argument is sharper when more voices contribute, and I’d rather be corrected publicly than continue holding a view that’s wrong.

If this resonated, follow me — I write weekly about Flutter, Dart, and the long-arc decisions that determine whether codebases scale or collapse under their own weight. The takes are opinionated; the comments are where the community sharpens them.

If you’ve shipped Flutter at scale with a BLoC-heavy codebase and your experience contradicts this article, I genuinely want to read your pushback. The case for BLoC-everywhere may be stronger than I’m giving it credit for. The conversation is what produces good engineering judgment, and I’m always open to having my mind changed.


메타데이터
post_id
980299d2ed08
slug
stop-writing-blocs-980299d2ed08
url
https://medium.com/@m.m.shahmeh/stop-writing-blocs-980299d2ed08
canonical_url
https://medium.com/@m.m.shahmeh/stop-writing-blocs-980299d2ed08
author_url
https://medium.com/@m.m.shahmeh
status
ok
fetched_at
2026-06-09 15:37:30