A testable Flutter architecture with Riverpod
setState mixes logic and UI in the same class; InheritedWidget propagates data well but says nothing about how it's created or swapped out in a test. Riverpod answers both: a dependency-injection container that produces reactive values, independent of the widget tree.
Providers and notifiers
Provider exposes a value; a Notifier exposes a mutable value along with the logic that drives it. With code generation ( riverpod_generator ), you annotate a class and build() returns the initial state:
ref.watch inside a provider creates a dependency : if apiClientProvider changes, currentUser recomputes automatically. This dependency graph is what replaces cascading manual setState calls.
Separating UI from logic
WidgetTester :
ref.watch(currentUserProvider) returns an AsyncValue , whose .when(data:, loading:, error:) covers all three states without a stray `isLoading` boolean .
Dependency injection and test overrides
InheritedWidget : every provider is overridable when the ProviderContainer is built. In a test, you inject a fake API client without touching production code:
addTearDown guarantees it's disposed. The Riverpod testing docs cover the pump and listener patterns in detail.
Why not setState or InheritedWidget
setState rebuilds the whole State and welds logic to the UI — impossible to test without rendering the widget. InheritedWidget shares a value but forces you to hand-write updateShouldNotify , handles neither async nor swapping, and leaks the moment you touch BuildContext . Riverpod moves state out of the tree , makes it lazy, memoized and auto-disposed ( autoDispose ), and turns overriding into the normal path for tests.
Good Flutter architecture isn't measured by the number of providers, but by this: can you test the logic without ever mounting a widget ? With Riverpod, the answer is yes by design.