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
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 alternativeBLoC 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
- A practical guide to Flutter Clean Architecture
- A maintainable GetX project structure
- Structure a Flutter Riverpod application
Primary reference
BLoC concepts. The source walkthrough above describes AppScaffold’s implementation; platform documentation describes the broader APIs and recommendations.