What is the difference between Provider, Riverpod, and BLoC?
3 min read
State Management
Theory — side by side
| Provider | Riverpod | BLoC | |
|---|---|---|---|
| Foundation | InheritedWidget + ChangeNotifier | Independent — no widget tree dependency | Stream<State> (or Cubit's setter) |
| API surface | context.watch / read / select | ref.watch / read / listen | BlocBuilder, BlocListener, context.read |
| Type safety | Runtime errors if you read the wrong type | Compile-time safe (especially with codegen) | Compile-time safe |
| Boilerplate | Low | Low (lower with riverpod_generator) | Higher — events, states, classes |
| Testing | Test the notifier as a class | Override providers in test scope | Test Bloc directly with bloc_test |
| Pattern | Flexible | Flexible | Strict: Event → Bloc → State |
| Learning curve | Easy | Medium | Steeper |
| Sweet spot | Small / medium apps | Most apps | Large apps, big teams, strict discipline |
Same feature, three styles
// ─── Provider ──────────────────────────────────────────────
class CounterNotifier extends ChangeNotifier {
int _c = 0;
int get count => _c;
void inc() { _c++; notifyListeners(); }
}
final counter = context.watch<CounterNotifier>();
context.read<CounterNotifier>().inc();
// ─── Riverpod (with codegen) ───────────────────────────────
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void inc() => state++;
}
// In a ConsumerWidget
final count = ref.watch(counterProvider);
ref.read(counterProvider.notifier).inc();
// ─── BLoC ──────────────────────────────────────────────────
sealed class CounterEvent {}
class Increment extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((e, emit) => emit(state + 1));
}
}
// Usage
context.read<CounterBloc>().add(Increment());
BlocBuilder<CounterBloc, int>(
builder: (ctx, count) => Text('$count'),
);
When to choose which
| Situation | Pick |
|---|---|
| Solo dev / small app / learning | Provider |
Want compile-time safety and no BuildContext headaches | Riverpod |
| Building a feature-rich app where you'll add many providers | Riverpod |
| Large team, strict architecture, audit trail of state transitions | BLoC |
| Heavy use of async streams, transactional flows | BLoC (its event/state model fits) |
| Need to migrate from one to another | Riverpod has the easiest interop story |
There's no "best" — they're tools with different trade-offs.
Common mistakes to avoid
// ❌ Picking the heaviest tool for the smallest job
// 5 screens, 2 toggles, and you stand up BLoC with events + states for each
// ✅ Start with setState or Provider; graduate when complexity demands it
// ❌ Mixing all three in one app for "comparison"
// → cognitive overhead, inconsistent patterns. Pick one.
// ❌ Treating Riverpod like Provider with renamed methods
// You miss the benefits — family, autoDispose, override-for-test, codegen
// ❌ Using BLoC without sealed states
// Switch statements over State become non-exhaustive → bugs
// ✅ Pair BLoC with sealed classes (Q13) for compile-time exhaustiveness
// ❌ Comparing "performance" — all three are fine
// Differences are ergonomic and architectural, not raw speed
Interview follow-ups
-
Why was Riverpod created if Provider already existed? To fix Provider's pain points:
ProviderNotFoundExceptionwas a runtime error, dependencies requiredBuildContext, composition between providers was awkward, and testing required real widget trees. Riverpod moves DI out of the widget tree, making it compile-time safe and easier to test. -
What's the BLoC pattern in one sentence? "Send events in, get states out — and the BLoC is a function that maps streams of one to streams of the other." That discipline forces explicit modelling of every state transition.
-
Cubit vs Bloc — what's the difference?
CubitisBloc's simpler cousin: instead of dispatching events, you call methods that emit new states directly. Less boilerplate, but you lose the explicit event log. Use Cubit for simple state, Bloc when you want event-driven traceability. -
Can you mix state-management approaches in one app? Technically yes —
setStatefor local UI, Provider/Riverpod for cross-screen, a BLoC for one complex flow. Just keep the primary approach consistent across the app, and document why exceptions exist. Mixing freely usually creates more confusion than benefit.
How helpful was this content?
Please sign in to rate this article.