Skip to content

Provider & Riverpod

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 changes
context.read<CartModel>().add('Book'); // one-off action, no rebuild

The 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 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 / StateNotifierProvider and StateProvider APIs are now legacy (deprecated in Riverpod 3) — you’ll still see them in existing code, but new code should use Notifier / NotifierProvider (or the @riverpod codegen).

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"]
Provider vs Riverpod at a glance

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.

What is the difference between context.watch and context.read in Provider?
What does calling notifyListeners() on a ChangeNotifier do?
What is Riverpod's key structural difference from Provider?
Which is the most common Provider bug?