Pick for the team and the problem size
State management arguments online are louder than most apps need. For small screens, local setState or a simple provider is fine. Complexity should match the product—not the Twitter discourse. Official overview: Flutter state management intro.
Provider — simple and fine to start
class Counter extends ChangeNotifier {
int value = 0;
void increment() {
value++;
notifyListeners();
}
}
// wrap app
ChangeNotifierProvider(create: (_) => Counter(), child: MyApp());
// read
context.watch<Counter>().value;
context.read<Counter>().increment();
Good for learning and growing apps. Can get messy if everything becomes a global ChangeNotifier without structure.
Bloc — explicit events and states
sealed class CounterEvent {}
class IncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<IncrementPressed>((event, emit) => emit(state + 1));
}
}
I like Bloc when events/states need to be explicit—especially with multiple async streams and clearer separation between UI and business logic. More boilerplate, more predictability. Package: bloclibrary.dev.
Riverpod — my default for new non-trivial apps
final counterProvider = StateProvider<int>((ref) => 0);
class CounterText extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
Riverpod gives compile-time safety and testable providers without some InheritedWidget footguns. Learning curve is real; payoff is cleaner dependency wiring. Docs: riverpod.dev.
When to graduate
- Start with
setState/ Provider for small features. - Move to Riverpod (or Bloc if the team already standardizes on it) when screens share async data and testing matters.
- Do not rewrite a working Provider app mid-launch because a blog said Riverpod is “modern.”
Testing note
Whatever you pick, keep business logic out of widgets when you can. Provider/Bloc/Riverpod all support overriding dependencies in tests—use that instead of hitting real APIs in widget tests.
Troubleshooting and common mistakes
Most failures I see are configuration and process issues, not “the framework is broken.” Slow down: reproduce on a clean environment, read the exact error, and change one variable at a time.
- Confirm you are on the documented major version of the tool you are following.
- Prefer official docs over random outdated blog snippets when commands disagree.
- Keep lockfiles committed so teammates and CI install the same dependency graph.
- Separate “works on my machine” fixes (PATH, SDK licenses, local services) from application bugs.
What to do next
Implement the smallest vertical slice from this article on a throwaway branch, then promote the patterns into your real app. Guides that stay theoretical never catch the auth, env, and deploy footguns that actually burn time.
Side-by-side decision notes
- Provider: fastest to teach; watch for god-notifiers
- Bloc: best when event sourcing-ish clarity helps the team
- Riverpod: best default for new apps that will grow and need tests
I avoid mixing all three in one app. Pick a house style and stick to it unless you are migrating deliberately.
Async data pattern (any library)
Separate “loading / data / error” in the state model. UI should not guess. Whether that is a Bloc state class or an AsyncValue in Riverpod, the idea is the same.