Flutter State Management: Provider vs Riverpod vs BLoC
Advanced4 minInnovialabs editorial team
In short
Provider fits small apps, Riverpod is a sensible default for most new projects, and BLoC pays off in large teams that need strict rules and a traceable flow of events.
The short answer
All three solve the same problem: keep data outside widgets and rebuild the UI when it changes. They differ in how strict they are and what that strictness costs.
- Provider is the simplest, a thin layer over
InheritedWidget. Good for small apps and prototypes. - Riverpod comes from the same author but does not depend on
BuildContext, catches more mistakes at compile time and makes overrides in tests easy. A sensible default. - BLoC only lets state change through events or Cubit methods. More code, but a predictable architecture for large teams.
State that belongs to a single widget (is a panel expanded, what is typed in a field) does not need any of them. setState is enough.
What it looks like in code
The same feature, a shopping cart, three ways.
Provider with ChangeNotifier:
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
List<Item> get items => List.unmodifiable(_items);
void add(Item item) {
_items.add(item);
notifyListeners();
}
}
// app root
ChangeNotifierProvider(create: (_) => CartModel(), child: const App());
// in a widget
final count = context.watch<CartModel>().items.length;
Riverpod with Notifier:
final cartProvider =
NotifierProvider<CartNotifier, List<Item>>(CartNotifier.new);
class CartNotifier extends Notifier<List<Item>> {
@override
List<Item> build() => [];
void add(Item item) => state = [...state, item];
}
// in a ConsumerWidget
final items = ref.watch(cartProvider);
ref.read(cartProvider.notifier).add(item);
BLoC in its lighter form, a Cubit:
class CartCubit extends Cubit<List<Item>> {
CartCubit() : super(const []);
void add(Item item) => emit([...state, item]);
}
BlocBuilder<CartCubit, List<Item>>(
builder: (context, items) => Text("${items.length}"),
);
A full Bloc adds event classes (ItemAdded, ItemRemoved) and on<Event> handlers. That is more code, but every state change has a name and is easy to trace in logs.
Side-by-side comparison
| Criterion | Provider | Riverpod | BLoC |
|---|---|---|---|
| Learning curve | Low | Medium | Medium to high |
| Boilerplate | Minimal | Some | The most |
Depends on BuildContext | Yes | No | Yes (to reach the bloc) |
| Missing provider errors | At runtime | Caught earlier | At runtime |
| Testing | Test the model class directly | ProviderContainer with overrides | bloc_test, assert state sequences |
| Async data | Manual | Built-in FutureProvider, AsyncValue | Loading states via events |
| Team size | 1 to 3 developers | Any, with conventions | Large teams, strict process |
Testability
Provider. A ChangeNotifier is a plain Dart class, easy to unit test. Dependencies are the harder part: you pass them through the constructor yourself.
Riverpod. Overrides are its strength. You can swap a repository for a fake without touching production code:
final container = ProviderContainer(
overrides: [repositoryProvider.overrideWithValue(FakeRepository())],
);
addTearDown(container.dispose);
container.read(cartProvider.notifier).add(item);
expect(container.read(cartProvider), [item]);
BLoC. The bloc_test package lets you write a test as "this event in, this list of states out". That shines for complex flows such as loading, error and retry.
Teams and scale
In large projects the real problem is not the library, it is inconsistency. If everyone writes state differently, any approach turns into a mess.
- BLoC enforces structure: event, handler, state. A new developer knows where to look for logic.
- Riverpod is more flexible, so the team must agree on conventions: where providers live, how they are named, when to use a
Notifierversus aFutureProvider. - Provider in a big codebase often ends with bloated
ChangeNotifierclasses holding dozens of fields and triggering wide rebuilds.
How to choose by project size
- Prototype, MVP, a handful of screens: Provider or Riverpod. Speed of validating the idea matters most.
- Mid-sized product, small team: Riverpod. Less code than BLoC, good testing story, async support out of the box.
- Large app, several teams, strict code review: BLoC, or Riverpod with a firm style guide. What the team already knows well often decides it.
- Existing project: do not rewrite everything to follow a trend. Build new modules with the new approach and migrate old ones gradually.
Common mistakes
- Business logic inside widgets. API calls and validation belong in the model, notifier or bloc.
- Making everything global. Form state on one screen does not need to be app-wide.
- Unnecessary rebuilds. Use selectors (
context.select,ref.watch(provider.select(...)),BlocSelector) so a widget reacts only to the field it shows. - Mixing all three in one project without a clear reason.