Provider & Riverpod
Provider: InheritedWidget with ergonomics
Section titled “Provider: InheritedWidget with ergonomics”Writing InheritedWidget by hand is boilerplate. Provider wraps that pattern with a clean API, and pairs it with ChangeNotifier — a simple class that holds state and calls notifyListeners() when it changes.
// 1. A model that notifies listeners when it changes.class CartModel extends ChangeNotifier { final List<String> _items = []; List<String> get items => List.unmodifiable(_items);
void add(String item) { _items.add(item); notifyListeners(); // tell listening widgets to rebuild }}
// 2. Provide it above the widgets that need it.ChangeNotifierProvider( create: (_) => CartModel(), child: const MyApp(),);
// 3. Read it. watch() rebuilds on change; read() does not.final cart = context.watch<CartModel>(); // rebuilds when cart changescontext.read<CartModel>().add('Book'); // one-off action, no rebuildThe watch vs read distinction is the whole game. context.watch<T>() subscribes — the widget rebuilds when the model notifies. context.read<T>() grabs it once without subscribing — use it in callbacks (like onPressed) where you want to act, not listen. Mixing them up is the most common Provider bug (either missing rebuilds or rebuilding too much).
Riverpod: providers without the tree
Section titled “Riverpod: providers without the tree”Riverpod is a reimagining by the same author that fixes Provider’s rough edges. Its key move: providers are top-level objects, not tied to the widget tree, which makes them compile-safe (you can’t read a provider that isn’t there) and easy to test and combine.
// A provider declared at top level — not in the widget tree.final counterProvider = NotifierProvider<Counter, int>(Counter.new);
class Counter extends Notifier<int> { @override int build() => 0; // the initial state, returned from build()
void increment() => state++; // reassigning `state` notifies listeners}
// In a ConsumerWidget, `ref` reads providers:class CounterView extends ConsumerWidget { const CounterView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(counterProvider); // rebuilds on change return TextButton( onPressed: () => ref.read(counterProvider.notifier).increment(), child: Text('$count'), ); }}A Notifier<T> returns its initial state from build() (not a constructor super(initial)), and mutating state notifies listeners. Notice the same watch/read split via ref. The difference from Provider is structural: no BuildContext lookup, no “provider not found” at runtime, and providers compose (one provider can ref.watch another).
For async state, the equivalent is AsyncNotifier / AsyncNotifierProvider, whose build() returns a Future. Riverpod also offers a @riverpod code-generation style that derives the provider for you from an annotated class or function.
The older
StateNotifier/StateNotifierProviderandStateProviderAPIs are now legacy (deprecated in Riverpod 3) — you’ll still see them in existing code, but new code should useNotifier/NotifierProvider(or the@riverpodcodegen).
Choosing between them
Section titled “Choosing between them”flowchart TB prov["Provider InheritedWidget + ChangeNotifier tied to the tree, runtime lookup"] --> use1["great for smaller apps, teams already using it"] rp["Riverpod top-level providers compile-safe, testable, composable"] --> use2["preferred for new apps, complex or testable state"]
Both are excellent. Provider is simpler and ubiquitous; Riverpod is more robust for larger, testable apps and is where the ecosystem is heading. Either one is a fine default over hand-rolled InheritedWidget for real shared state.