State management is one of the first big decisions you make in a Flutter project. Get it wrong and you'll be refactoring for weeks. Get it right and the architecture practically writes itself.
Here's my honest take after using all three in production.
The Three Contenders
- Provider — simple, built on InheritedWidget, great for smaller apps
- Bloc — strict, event-driven, scales well in large teams
- Riverpod — the evolution of Provider, flexible and testable
Provider
Provider is where most Flutter devs start. It wraps InheritedWidget in a friendly API and works well for straightforward cases.
// Define the state
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
// Provide it
ChangeNotifierProvider(
create: (_) => CounterNotifier(),
child: MyApp(),
)
// Consume it
final counter = context.watch<CounterNotifier>();
Text('${counter.count}')When to use it: Small to medium apps, quick prototypes, teams new to Flutter.
Where it breaks down: Context coupling gets messy in large apps. Testing requires a widget tree. Handling async state is more verbose than it should be.
Bloc
Bloc enforces a strict separation between UI and business logic using events and states.
// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
// States
abstract class CounterState {}
class CounterValue extends CounterState {
final int count;
CounterValue(this.count);
}
// Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterValue(0)) {
on<Increment>((event, emit) {
final current = (state as CounterValue).count;
emit(CounterValue(current + 1));
});
}
}
// UI
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
if (state is CounterValue) {
return Text('${state.count}');
}
return const SizedBox();
},
)When to use it: Large teams, complex business logic, apps where auditability matters (you can log every event/state transition).
Where it breaks down: Significant boilerplate. Overkill for simple features. New devs need time to get comfortable with the pattern.
Riverpod
Riverpod is Provider rewritten from scratch. It removes the context dependency entirely and makes async state a first-class citizen.
// Define a provider
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}
// Consume it — no BuildContext required for the logic
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}Async is where Riverpod really shines:
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
return await ref.read(apiServiceProvider).getUser(userId);
});
// In widget
ref.watch(userProvider(userId)).when(
data: (user) => Text(user.name),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
);When to use it: Most new Flutter projects. Excellent for async-heavy apps (API calls, streams). Great testability without a widget tree.
Where it breaks down: The learning curve is real — there are many provider types (Provider, StateProvider, StateNotifierProvider, FutureProvider, StreamProvider). The mental model takes a day or two to click.
My Pick
For most projects I reach for Riverpod. The combination of no-context providers, built-in async support, and clean testability outweighs the learning curve.
I use Bloc when the team is large and consistency matters more than speed — the rigid structure pays dividends when 5+ people are writing features in parallel.
Provider is still my go-to for quick internal tools or when onboarding a Flutter newcomer — less to explain upfront.
Whichever you pick, commit to it. Mixing state management solutions in one app is worse than choosing the "wrong" one consistently.
Quick Reference
| Provider | Bloc | Riverpod | |
|---|---|---|---|
| Learning curve | Low | High | Medium |
| Boilerplate | Low | High | Medium |
| Async support | Manual | Manual | Built-in |
| Testability | OK | Great | Great |
| Best for | Small apps | Large teams | Most apps |