TL;DR
For widgets that own their state, setState is correct. For app-wide state in 2026, Riverpod has the best ergonomics. For event-sourced enterprise apps, Bloc still wins. Provider is fine if you already have it. Signals (via signals_flutter, solidart) are the rising option for fine-grained reactivity. Pick by team scale and app boundaries, not by Twitter discourse.
The honest hierarchy
Every state management decision sits on three axes: scope (one widget, one screen, the whole app), team scale (solo, 5 engineers, 50 engineers), and auditability (do you need a log of every state transition for compliance or replayability). Most state management debates ignore those axes and argue Twitter aesthetics.
Tier 1: setState
If your state lives inside one widget and is never read by widgets outside, setState is the right call. It is framework-native, zero dependencies, zero indirection. The reason engineers reach past it too early is that they confuse "state inside the widget" with "state inside the file." Start with setState; lift the moment a sibling widget needs to read a value.
Tier 2: InheritedWidget
Every state management library is, under the hood, an InheritedWidget with extra ergonomics. Provider wraps it. Riverpod wraps a smarter version. Theme.of, MediaQuery.of, Navigator.of: all InheritedWidgets.
Tier 3: Provider (the safe default)
Provider was the official recommendation from 2018 to roughly 2022 and is still in heavy production use. It wraps InheritedWidget into a familiar API: ChangeNotifierProvider, context.watch, context.read, Selector for fine-grained rebuilds. If you have a Provider codebase, keep it. Migration mid-project rarely pays back.
Tier 4: Riverpod (the 2026 default for new apps)
Riverpod is what Provider's author wrote when he had a chance to start over. The headline differences:
-
Compile-time safety: a provider that does not exist will not compile. No
ProviderNotFoundExceptionat runtime. -
Code generation:
@riverpodannotation produces type-safe providers, eliminating boilerplate. -
AsyncNotifier: first-class support for async state with
AsyncValue(data, loading, error variants). - Family providers: parameterized providers (per user ID, per item ID) without manual disposal.
- Auto-dispose: providers can release resources automatically when no widget reads them.
Tier 5: Bloc (the enterprise pattern)
Bloc separates events (intents) from state (results). UI dispatches events, the Bloc maps events to states, the UI rebuilds on state changes. Where Bloc shines: large teams (10+ engineers per app), strict architectural review, compliance contexts where every state transition must be logged and replayable.
Tier 6: signals (the rising star)
Signals (in Flutter via packages like signals_flutter and solidart) implement the SolidJS / Vue / Svelte approach: a value wrapped in an object that automatically tracks its readers and only rebuilds those readers. No watch, no ref, no Provider scope.
Decision matrix
| Context | Recommendation in 2026 |
|---|---|
| State inside one widget | setState |
| Solo dev, new app, < 10 screens | Riverpod (or signals) |
| 5-engineer team, new app | Riverpod |
| Existing Provider codebase | Stay on Provider |
| 20+ engineer team, enterprise | Bloc |
| Compliance / audit log needed | Bloc (or Redux for time travel) |
| Real-time collab, complex async | Bloc or custom RxDart streams |
The trap to avoid
Do not pick state management based on a Twitter thread. Pick by walking through three questions:
- Where does this specific piece of state live, and who reads it?
- How many engineers will touch this code in the next year?
- Does anyone outside the engineering team need to audit how this state changed?
The full version with what changed in 2026 vs 2024 and concrete code patterns for each tier is on my site:
Read the complete article on ishaqhassan.dev
I am a Flutter Framework Contributor with 6 merged PRs into flutter/flutter and Engineering Manager at DigitalHire. More writing at ishaqhassan.dev/blog/.
Top comments (0)