flutter · 3 min read

BLoC vs Riverpod: choose by how your team works

By Vivek Patel · Updated

Both BLoC and Riverpod can support a maintainable Flutter app. Choose the state model your team can explain and test. BLoC makes transitions explicit through events and states; Riverpod represents dependencies and state through providers. Neither substitutes for a clear repository boundary.

How the pieces connect

User action→ State owner→ Repository→ New UI state

An illustrative folder structure

This structure explains the architecture. Exact generated paths are shown in the source walkthrough below.

features/catalog/
  data/catalog_repository.dart
  presentation/
    catalog_page.dart
    state/
      catalog_bloc.dart      # BLoC option
      catalog_provider.dart  # Riverpod alternative

BLoC favors explicit event vocabulary

Events such as SearchSubmitted and NextPageRequested provide a shared language between UI and state logic. This is useful for complex workflows and teams that value consistent transition patterns. The cost is more files and careful management of event concurrency. Cubit removes the event layer for simpler state changes.

Riverpod models a dependency graph

Providers compose cached values and services, while Notifier and AsyncNotifier own changing state. A screen watches only what it needs. Provider overrides make repository substitution straightforward in tests. Pay attention to provider lifetime and invalidation: a provider is not automatically a durable store.

Handle asynchronous races deliberately

Neither library makes search requests arrive in order. Decide whether to cancel stale requests, ignore older results, or serialize operations. Represent loading and failures alongside data so a failed refresh need not erase a usable list. Test two requests completing in the opposite order.

Choose one primary state system

Use BLoC when explicit events and transition traces fit the team. Use Riverpod when dependency composition and async derived state dominate. Avoid mixing both throughout the same feature without a clear boundary. Migrate one feature at a time through existing repository contracts.

Inside the generated starter

Compare the same counter operation in the two generated state owners. Keep the repository and UI requirement fixed so the comparison is about state ownership rather than unrelated application features.

These source files are produced from the same templates used by the generator, with the configuration below. They are starter examples; the observations identify work still needed for a real feature.

lib/features/home/state/counter_bloc.dart

import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/repositories/counter_repository.dart';
import 'counter_event.dart';
import 'counter_state.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc(this.repository) : super(const CounterState()) {
    on<CounterIncremented>((event, emit) async {
      final next = state.count + 1;
      await repository.save(next);
      emit(CounterState(count: next));
    });
  }
  final CounterRepository repository;
}

lib/features/home/state/counter_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/dependency_injection/container.dart';

final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterNotifier extends Notifier<int> {
  @override
  int build() => 0;
  Future<void> increment() async { final next = state + 1; await container.counterRepository.save(next); state = next; }
}

What to notice

The BLoC version receives its repository through its constructor. An event handler waits for save before emitting CounterState. That constructor provides a direct place to inject a fake for a state test.

The Riverpod starter uses a NotifierProvider and obtains the repository from the application container. It is not yet a repository-provider graph. To override persistence independently in tests, introduce a repository provider and read it through ref; do not assume the starter already provides that boundary.

Both examples are deliberately small and lack async loading/error models. Compare how your team would represent a failed refresh with previously loaded data, rather than choosing by line count in this counter example.

Extend it and verify the result

Implement a delayed save and a failing save in each version. Give both screens the same disabled/loading behavior. Then test two actions before the first save finishes. Choose BLoC if your team finds the event vocabulary clearer, or Riverpod if explicit provider dependencies make the larger feature easier to compose.

  • The display updates after a successful save.
  • A failed save leaves the prior value available.
  • The repository can be replaced without editing the widget.
  • Disposing the screen does not leave unhandled asynchronous errors.

These are exercises to add to your application tests, not claims that the starter already handles every case.

Try this configuration

Clean Architecture · Riverpod · Riverpod DI · Dio

The generator includes a working counter example with a repository contract, selected dependencies, navigation, and local setup instructions. Extend the example around your own domain before shipping.

Configure your Flutter project →

For prerequisites, exact setup commands, expected behavior, and build troubleshooting, follow the flutter download-to-first-run walkthrough. Check the validation record for the configurations actually checked.

Continue reading

Primary reference

BLoC concepts. The source walkthrough above describes AppScaffold’s implementation; platform documentation describes the broader APIs and recommendations.