Provider is a wrapper around Flutter’s built-in InheritedWidget that makes sharing state across the widget tree far less boilerplate-heavy. It’s built on ChangeNotifier, a class already included in the Flutter SDK, which is why Provider tends to be many teams’ first step beyond setState.
Setup
dependencies:
provider: ^6.1.0
Step 1: Define a ChangeNotifier
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
List<Item> get items => _items;
void add(Item item) {
_items.add(item);
notifyListeners();
}
}
Calling notifyListeners() is what triggers a rebuild of any widget listening to this model — it’s the ChangeNotifier equivalent of setState, except it can notify widgets anywhere in the tree, not just the one that owns it.
Step 2: Provide It Above Where It’s Needed
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
Step 3: Read and Write It From Descendants
// Reading, and rebuilding when the model changes:
Widget build(BuildContext context) {
final cart = context.watch<CartModel>();
return Text('${cart.items.length} items');
}
// Writing, without listening for changes:
onPressed: () {
context.read<CartModel>().add(item);
}
This is the pattern that trips people up most often: context.watch() subscribes to changes and rebuilds the widget when they happen — use it inside build(). context.read() gets the current value once, without subscribing — use it inside callbacks like onPressed, where you don’t want the whole callback re-created on every notification.
Avoiding Unnecessary Rebuilds
context.watch<CartModel>() rebuilds the whole widget whenever anything in CartModel changes, even a property this particular widget doesn’t display. context.select() narrows that down to one field:
final itemCount = context.select((CartModel c) => c.items.length);
Now this widget only rebuilds when items.length specifically changes, not on every change to the cart.
Provider’s Limits
Provider is simple and has almost no learning curve if you already know ChangeNotifier, but it has no built-in concept of asynchronous loading states, no compile-time safety for missing providers (a missing ChangeNotifierProvider higher up throws at runtime, like SwiftUI’s @EnvironmentObject), and testing requires wrapping widgets in a provider tree manually. Riverpod, built by the same author, addresses all three while keeping a similar mental model.