Skip to content
ALI HAITHAM·TECH·responds in <1h
  • Homestart here
  • Workcase studies, projects
  • Serviceswhat I will build for you
  • Writingessays, series
  • Traincourses, labs, cohorts
  • Aboutthe engineer behind this site
Sign inStart a project
ALI HAITHAM · TECH
  • Home↗
  • Work↗
  • Services↗
  • Writing↗
  • Train↗
  • About↗
Sign inStart a project
Online
ALI HAITHAM·TECH

Engineering studio. Damascus, GMT+3.

aliyosef.online

Studio

  • Work
  • Writing
  • Training
  • About
  • Contact me

Portal

  • Sign in
  • Open a ticket
  • Track project
  • My account

Resources

  • Docs
  • Status
  • Changelog
  • Brand kit
  • Privacy
  • Terms

Newsletter

Field notes and tech news. Weekly. No fluff.

Free. Unsubscribe anytime.

Find me elsewhere
© 2026 Ali Haitham Yosef. All rights reserved.Hand-built in React 19. No frameworks of frameworks.Last deployed · 2026-05-08All systems operational
  1. Home/
  2. Writing/
  3. Flutter
Flutter

Riverpod vs Bloc, a year later

Receipts from production. Where each fits, where each fails, and what we wish someone had told us.

February 10, 2026·10 min·by Ali Yosef

Loading essay…

ShareXLinkedInRSS
Get the next essay in your inbox.New posts every Sunday morning. Engineering, in plain language. Free, unsubscribe anytime.Subscribe→
AY
Ali Haitham Yosef

Engineer and writer. Builder of multi-tenant platforms, sync engines, and quietly reliable systems. 600+ shipped projects since 2015.

SubscribeStart a project→

Read next

  • OF
    Series
    Mar 6, 2026·Flutter

    Offline-first Flutter, part one: the boot

    Series opener. What the app does in its first 800ms when there is no network.

    13 min
  • DM
    Feb 26, 2026·Flutter

    Drift migrations without losing data

    The before, the during, the rollback. Pre-flight checks that have saved us five times.

    9 min
  • SR
    Feb 18, 2026·Flutter

    The cost of state restoration

    Bringing the app back to where the user left it. The cheap version vs the expensive version.

    7 min
←Back to all essays

A year ago we had two real Flutter apps in production, one on Bloc and one on Riverpod, both written by the same team. This is what we learned. Both libraries are good. They optimise for different things and one of those things matters more than the other for most teams.

What each one is actually for#

Skip the marketing. Here is the structural difference:

  • Bloc treats state as a stream of events that produce a stream of states. The mental model is "input event in, output state out." It is opinionated about how you write the producer (a Bloc class with event handlers) and the consumer (BlocBuilder / BlocListener).
  • Riverpod treats state as a graph of providers, each of which computes a value from other providers. The mental model is "this thing depends on that thing depends on that other thing." It is opinionated about reactivity and dependency, agnostic about how the state inside a provider is updated.

Bloc draws a line between events, state, and rendering. Riverpod draws a graph of values that fan into rendering. Both work. The difference is what they prevent you from doing.

Where Bloc shines#

Two situations:

1. Workflows with named transitions#

If your screen is a state machine (an OTP flow, a checkout, an onboarding wizard), Bloc's on<Event> handlers map cleanly to the states the user can be in. Reading the bloc file is reading the state machine. New team members can navigate it.

class OtpBloc extends Bloc<OtpEvent, OtpState> {
  OtpBloc() : super(const OtpInitial()) {
    on<OtpRequested>(_onRequested);
    on<OtpResent>(_onResent);
    on<OtpEntered>(_onEntered);
    on<OtpVerified>(_onVerified);
    on<OtpExpired>(_onExpired);
  }
}

Tests against a Bloc are unusually good: send events, assert state sequences, done. We had > 90 % branch coverage on our Bloc files without trying.

2. Audit / event-sourcing affinity#

When the events are themselves valuable (analytics, audit, replay), Bloc gives them to you for free. We piped Bloc events into a single analytics sink and got user-flow tracking with zero extra code per screen.

Where Riverpod shines#

Three situations:

1. Cross-cutting state with nuanced lifetimes#

A Riverpod provider can be autoDispose, family-keyed, scoped to a route, scoped to the app. The lifetime of state in Riverpod is a first-class concept; in Bloc, you wrap providers around the widget tree and pray.

final invoiceProvider = FutureProvider.family.autoDispose<Invoice, String>(
  (ref, id) async {
    final db = ref.watch(databaseProvider);
    return db.invoices.byId(id);
  },
);

The provider is created when the screen needs it, disposed when the screen leaves, family-keyed by id so two open screens with two different ids do not collide. Doing this in Bloc requires manual provider scoping in the widget tree.

2. Derived state with deep graphs#

When state value B depends on A, and C depends on A and B, and D depends on B, the Riverpod graph reads cleanly:

final filtersProvider = StateProvider<InvoiceFilters>((ref) => InvoiceFilters.empty());
final allInvoicesProvider = Provider((ref) => ref.watch(databaseProvider).invoices.all);
final filteredInvoicesProvider = Provider((ref) {
  final filters = ref.watch(filtersProvider);
  final all = ref.watch(allInvoicesProvider);
  return all.where(filters.matches);
});
final summaryProvider = Provider((ref) {
  final filtered = ref.watch(filteredInvoicesProvider);
  return InvoiceSummary.from(filtered);
});

The same setup in Bloc requires events that flow through a sequence of blocs or one large bloc with many state branches. Either way is more code.

3. The "I just need a counter" case#

For trivial state that nobody will ever need to test, Riverpod's StateProvider is one line. Bloc's minimum is a Bloc class with at least one event and one state. The ceremony adds up over a week.

Where each one fails#

Bloc, after about 30 screens#

Bloc's "wrap the consumer with a BlocProvider" pattern starts to generate widget trees with five layers of providers above each screen. We had files where the MultiBlocProvider was longer than the rest of the screen. Code review fatigue is real.

Riverpod, on workflow-heavy screens#

The graph model is great until the screen is fundamentally a state machine. We had a Riverpod-backed checkout flow where the "current step" was a StateProvider, and every transition manually flipped it. Reading that flow back six months later was harder than reading a bloc would have been. The transitions were scattered across the file.

What we ended up doing#

The lesson is not "pick one." It is "pick per surface."

SurfaceLibrary
Domain caches (invoices, contacts, products)Riverpod
Settings, user prefsRiverpod
Auth flow, OTP, onboardingBloc
Checkout, multi-step formsBloc
Filter panels, search resultsRiverpod
Connectivity / sync stateRiverpod
Screen-local trivial state (counters, toggles)Riverpod

The teams that argue about "which one wins" tend to be teams that have committed to one for the whole app. Once you accept that different surfaces have different shapes, the choice gets easier.

What we wish we had been told#

Three things, with a year of hindsight:

  1. Both libraries are excellent. The "wrong" choice is at most a 30 % cost in code volume on the surfaces it suits less. The right choice everywhere is mostly a 0 % win because both work.
  2. Test the hardest screen first. Spend a day building one screen in each library. The screen that is easier to read in six months is the right choice for that surface. Generalise carefully.
  3. Migration between them is cheap. A Riverpod provider can be wrapped to look like a Bloc consumer and vice versa. We migrated three screens between the two over a year and never lost a day to it. Picking is not a one-way door.

The next part of this series jumps to a different surface: AI-paired engineering in production, what it actually looks like outside the demo. Or check the series page for the rest of the offline-Flutter arc.