A Gentle Introduction to Provider
What state management packages solve, and a first look at Provider as a way past prop drilling.
읽는 데 2분
The last lesson ended with prop drilling — passing state down through widgets that don't use it, just to reach ones that do. Provider is one of the most widely used packages for solving this, and it's a good first state management tool to learn because it builds directly on concepts you already know: InheritedWidget-style lookups (like Theme.of(context)), plus ordinary Dart objects.
The core idea
Instead of passing state through constructor parameters at every level, you place it once, high in the tree, and any descendant widget can reach up and grab it directly — no matter how many layers separate them.
Defining a model
State that multiple widgets share is modeled as a plain Dart class that extends ChangeNotifier, calling notifyListeners() whenever something changes:
class CartModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void add() {
_count++;
notifyListeners();
}
}This class knows nothing about widgets — it's the same kind of class you'd write in plain Dart. notifyListeners() is the one addition: it tells anything listening (via Provider) that it should rebuild.
Providing it to the tree
Wrap the part of the tree that needs access with a ChangeNotifierProvider, typically near the top of the app:
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}Anything below MyApp in the tree can now access this single CartModel instance, regardless of nesting depth.
Reading and updating from anywhere
class CartBadge extends StatelessWidget {
const CartBadge({super.key});
@override
Widget build(BuildContext context) {
final cart = context.watch<CartModel>();
return Text('Cart: ${cart.count}');
}
}
class AddToCartButton extends StatelessWidget {
const AddToCartButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<CartModel>().add(),
child: const Text('Add to cart'),
);
}
}The distinction between context.watch and context.read matters: watch subscribes this widget to rebuild whenever notifyListeners() fires — use it wherever you're displaying the value. read grabs the current instance without subscribing to future changes — use it for one-off actions like a button press, where rebuilding on every future change would be wasted work.
CartBadge and AddToCartButton no longer need any constructor parameters or callbacks passed down from a parent — both reach directly into the shared CartModel, solving the prop-drilling problem from the previous lesson entirely.
Why start here, not with something more advanced
Packages like Riverpod build on the same underlying idea — shared, observable state reachable from anywhere in the tree — while fixing some of Provider's rough edges (like needing BuildContext to read state, and less compile-time safety). Learning Provider first is still worthwhile: the concepts — a model that notifies listeners, widgets that watch versus read, providing state high and consuming it low — transfer directly, and Provider's smaller API surface makes those concepts easier to see clearly before adding a more sophisticated tool on top.