Provider vs Riverpod vs Bloc: Which Flutter State Management to Choose?
An honest comparison of Provider, Riverpod and Bloc for Flutter. The same counter-and-cart example in all three, the pros and cons of each, and a simple way to decide for your app.
By Yaqoob Developer · · 5 min read
- #Flutter
- #State Management
- #Provider
- #Riverpod
- #Bloc
"Which state management should I use?" is the most common question in Flutter. Search for it and you will find strong opinions and long arguments.
This post is practical instead. We build the same small feature in Provider, Riverpod and Bloc, compare the code side by side, and end with a simple guide to choose.
If you are still deciding whether you need a package at all, read setState vs Provider first. For a single screen, setState is often enough.
The feature
A shopping cart:
- A list of item names
- An "Add" action
- The total count shown in the app bar, on a different widget than the list
The count and the list are in different parts of the widget tree. That is exactly the situation where you need shared state.
1. Provider
Provider is the oldest of the three and was recommended by the Flutter team for years. You put a ChangeNotifier above your widgets and listen to it below.
flutter pub add provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
int get count => _items.length;
void add(String item) {
_items.add(item);
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartModel(),
child: const MaterialApp(home: CartPage()),
),
);
}
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context) {
final cart = context.watch<CartModel>();
return Scaffold(
appBar: AppBar(title: Text('Cart (${cart.count})')),
body: ListView(
children: [for (final item in cart.items) ListTile(title: Text(item))],
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CartModel>().add('Item ${cart.count + 1}'),
child: const Icon(Icons.add),
),
);
}
}
context.watchrebuilds this widget when the cart changes.context.readgets the cart without listening, which is what you want inside onPressed.- You must remember to call
notifyListeners()after every change.
Good: small API, easy to learn, lots of tutorials, used in many existing apps.
Watch out for: a missing provider is a runtime error ("Could not find the correct Provider"), not a compile error. Combining many providers that depend on each other gets messy.
2. Riverpod
Riverpod was made by the same author as Provider to fix those problems. Providers are global declarations, not widgets in the tree, so a missing provider is impossible and providers can depend on each other cleanly.
flutter pub add flutter_riverpod
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CartNotifier extends Notifier<List<String>> {
@override
List<String> build() => [];
void add(String item) => state = [...state, item];
}
final cartProvider = NotifierProvider<CartNotifier, List<String>>(CartNotifier.new);
void main() {
runApp(const ProviderScope(child: MaterialApp(home: CartPage())));
}
class CartPage extends ConsumerWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(cartProvider);
return Scaffold(
appBar: AppBar(title: Text('Cart (${items.length})')),
body: ListView(
children: [for (final item in items) ListTile(title: Text(item))],
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(cartProvider.notifier).add('Item ${items.length + 1}'),
child: const Icon(Icons.add),
),
);
}
}
- State is immutable: instead of changing the list, you assign a new one (
state = [...state, item]). Riverpod notices and rebuilds listeners. - Widgets extend ConsumerWidget and get a
refto read providers. - No
notifyListeners()to forget.
Good: compile-safe, great for async data (FutureProvider and AsyncNotifier handle loading and error states for you), easy to test, scales well.
Watch out for: more concepts to learn (Notifier, AsyncValue, ref, providers that auto-dispose). The API has changed between major versions, so check that a tutorial matches the version you installed.
3. Bloc
Bloc is built around a strict rule: the UI sends events or calls methods, and the bloc emits new states. It comes in two forms. Cubit is the simple one and is all most features need.
flutter pub add flutter_bloc
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CartCubit extends Cubit<List<String>> {
CartCubit() : super(const []);
void add(String item) => emit([...state, item]);
}
void main() {
runApp(
BlocProvider(
create: (_) => CartCubit(),
child: const MaterialApp(home: CartPage()),
),
);
}
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CartCubit, List<String>>(
builder: (context, items) => Scaffold(
appBar: AppBar(title: Text('Cart (${items.length})')),
body: ListView(
children: [for (final item in items) ListTile(title: Text(item))],
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CartCubit>().add('Item ${items.length + 1}'),
child: const Icon(Icons.add),
),
),
);
}
}
- emit publishes a new state. Like Riverpod, states are immutable.
- BlocBuilder rebuilds when the state changes. BlocListener is for one-time reactions, like showing a SnackBar or navigating.
- The full Bloc (not Cubit) uses event classes, which gives you a log of everything that happened. Useful in big apps, extra code in small ones.
Good: very predictable, strict separation of UI and logic, excellent tooling and testing, popular with large teams and companies.
Watch out for: the most code of the three, especially with full Bloc and events. It can feel heavy for small apps.
Side by side
| Provider | Riverpod | Bloc / Cubit | |
|---|---|---|---|
| Learning curve | Easiest | Medium | Medium (Cubit) to steeper (Bloc) |
| Code size | Small | Small | Medium to large |
| Missing-provider errors | At runtime | Impossible | At runtime |
| Async loading/error states | Manual | Built in (AsyncValue) | Manual states |
| Testing | Good | Very good | Very good |
| Common in job listings | Yes | Growing fast | Yes, especially larger companies |
So which one?
A simple way to decide:
- Learning Flutter or building a small app? Start with Provider. It teaches the core idea (shared state, listening, rebuilding) with the least code. Everything you learn carries over.
- Starting a new app that will grow, especially with lots of API data? Choose Riverpod. AsyncValue alone saves you a lot of loading-and-error boilerplate.
- Joining a team, or the app has complex flows and many developers? Bloc (start with Cubit). Its strictness pays off when many people touch the same code.
And the most important rule: pick one and use it consistently. An app that mixes three state management approaches is harder to work on than an app that uses any one of them well.
Common mistakes
Calling watch inside onPressed. Use read in callbacks and watch in build. Watching in a callback causes errors or extra rebuilds.
Changing a list in place with Riverpod or Bloc. state.add(item) changes the same list object, so nothing rebuilds. Always create a new list: [...state, item].
Putting everything in global state. Text field contents, a toggle on one screen, an animation: these belong in the widget's own state. Shared state is for data that several screens need.
Summary
- Provider: simplest, great for learning and small apps.
- Riverpod: compile-safe and excellent for async data; a strong default for new apps.
- Bloc/Cubit: strict and predictable; great for teams and complex apps.
- Choose one, keep state immutable in Riverpod and Bloc, and only share what needs sharing.
Want to practice? Take any screen from my Flutter UI projects, like the chat app messages UI, and move its data into the state management you chose.
Keep reading
More articles
· 4 min read
Flutter Buttons: Custom Styles, Gradient Buttons and Loading States
Style Flutter buttons the right way. Learn FilledButton, ElevatedButton, OutlinedButton and TextButton, customise shape and colors with styleFrom, and build gradient and loading buttons.
· 5 min read
Flutter Splash Screen and App Icon: The Complete Setup Guide
Replace the default Flutter icon and white launch screen. Set up a native splash screen with flutter_native_splash, including Android 12, and add every iOS and Android app icon size the easy way.
· 5 min read
Parse JSON in Flutter: Create Dart Model Classes (fromJson and toJson)
Stop using Map<String, dynamic> everywhere. Learn how to turn JSON into Dart model classes with fromJson and toJson, handle nested objects, lists and null values, and generate models instantly.