Volver a todos los artículos
    flutter
    state-management
    bloc
    riverpod

    Bloc vs Riverpod: Picking State Management That Won't Hurt You Later

    How to choose between Bloc and Riverpod for a real Flutter app — boundaries, testing cost, and the patterns that actually scale past 50 screens.

    Alberto del Viejo30 de abril de 20267 min de lectura

    The Bloc-vs-Riverpod question gets framed as preference. It isn't. In a six-month project the cost difference shows up in three places: how new engineers ramp, how much test infrastructure you write, and what happens when a feature crosses three screens and four data sources at once. After shipping both at production scale, here's the version of this comparison I wish someone had given me before my first 50-screen Flutter app.

    When is Bloc the right call for a production Flutter app?

    Bloc earns its keep when state changes follow clear, named events and you need a transcript of what happened. The pattern of event → bloc → state makes the lifecycle of a feature legible — junior engineers can read a screen and predict every transition without spelunking. That clarity costs you boilerplate, and the boilerplate is real, but for teams above five engineers it pays back fast.

    The discriminator I use: if your domain has natural verbs (SubmitOrder, RefreshFeed, RetryPayment), Bloc fits the grain. If state is mostly derived from queries against a data layer, Bloc starts feeling like a translation layer over what's really happening, and you fight the framework.

    // Bloc reads as a transcript of what the screen can do.
    class OrderBloc extends Bloc<OrderEvent, OrderState> {
      OrderBloc(this._repo) : super(OrderState.initial()) {
        on<OrderSubmitted>(_onSubmitted);
        on<OrderRetried>(_onRetried);
      }
    
      final OrderRepository _repo;
    
      Future<void> _onSubmitted(OrderSubmitted e, Emitter<OrderState> emit) async {
        emit(state.copyWith(status: OrderStatus.submitting));
        final result = await _repo.submit(e.cart);
        emit(result.fold(
          (err) => state.copyWith(status: OrderStatus.failed, error: err),
          (id)  => state.copyWith(status: OrderStatus.confirmed, id: id),
        ));
      }
    }
    

    When does Riverpod outperform Bloc on the same problem?

    Riverpod wins when most of your screens are reading derived data and your problem is "how do I keep these views in sync with the data layer." The compositional model — providers that depend on other providers — gives you cache invalidation, fine-grained rebuilds, and select-style filtering without writing infrastructure for it. Async state is built into the framework instead of bolted on.

    The other underrated win: Riverpod tests are dramatically cheaper. You override one provider, wrap your widget in a ProviderScope, and assert. No mock-event-bus dance, no setup files. On projects with strict coverage targets that compounds.

    // Riverpod composes — the screen reads what it needs, no events to plumb.
    final orderRepoProvider = Provider((ref) => OrderRepository());
    
    final submitOrderProvider = AsyncNotifierProvider.autoDispose
        .family<SubmitOrderController, void, Cart>(SubmitOrderController.new);
    
    class SubmitOrderController extends AutoDisposeFamilyAsyncNotifier<void, Cart> {
      @override
      Future<void> build(Cart cart) async {} // idle until called
    
      Future<void> submit() async {
        state = const AsyncLoading();
        state = await AsyncValue.guard(() => ref.read(orderRepoProvider).submit(arg));
      }
    }
    

    What's the actual cost of switching mid-project?

    It's higher than the migration guides admit, and the cost isn't the rewrite — it's the year of mixed code. You'll have screens on the old system, screens on the new one, and shared services that need to satisfy both. The team's mental model fractures. Code review slows down because reviewers have to context-switch between paradigms.

    If you're asking the migration question, the better question is usually: which one of these screens is bleeding us right now, and can we contain the new pattern there instead of declaring a project-wide pivot? In every migration I've seen end well, the team picked one bounded module, made the new approach load-bearing for that module, and only then talked about expanding.

    How do I make a defensible choice on day one?

    Three concrete checks, in order:

    1. List the top ten state transitions in your app. If they read like verbs (submit, retry, refresh, navigate), lean Bloc. If they read like queries (current user, active filters, derived totals), lean Riverpod.
    2. Check your team's familiarity. If three people on the team have shipped Bloc before, the velocity argument outweighs the architectural one for the first six months. The "best" tool you can't move fast in is the wrong tool.
    3. Look at the testing budget. If you're targeting >70% widget-test coverage, Riverpod's override-based testing will save weeks across the project. If you mostly write integration tests through the UI, the difference shrinks.

    There is no neutral default. Picking either of them deliberately, with the trade-offs named, beats picking the trendier one and discovering the cost six sprints in.

    Practical takeaway

    Pick Bloc when the domain has clear verbs and the team needs a strict, legible structure. Pick Riverpod when most state is derived data and you want fine-grained reactivity without writing the plumbing yourself. Don't mix them on the same surface area. Don't migrate without naming the bounded module that will change first.

    How FlutterLab approaches this

    At FlutterLab we've shipped both across roughly 40 Flutter projects and the call usually comes down to the team and the domain shape, not the framework's resume. Happy to look at your specific case if it'd help.

    Frequently asked questions

    Is Bloc deprecated or losing traction? No. It's actively maintained, used in large production apps, and remains the default in many Flutter codebases. "Trendiness" is a poor proxy for fit.

    Can I use Riverpod and Bloc together in the same app? Technically yes, in practice it's a tax on every new engineer. Pick one as the default and only break the rule with a documented reason.

    Does Riverpod replace setState for small apps? For an app with one or two screens, setState is correct. Don't pull in a state-management framework before you have state worth managing.

    Which one performs better at scale? In benchmarks both are fast enough that you'll hit network, list virtualization, or image decoding limits long before you hit framework overhead. Performance is rarely the deciding factor.