Lesson 17 / 25

Provider and ChangeNotifier

Simple, widely used app state.

Listenable models

The provider package wraps InheritedWidget in a friendlier API. Put state and logic in a class extending ChangeNotifier and call notifyListeners() after changes. Expose it with ChangeNotifierProvider(create: ...) above the widgets that need it (or MultiProvider for several). In build, context.watch<Cart>() reads the model and rebuilds on change; in callbacks, context.read<Cart>() reads without subscribing; context.select<Cart, int>((c) => c.count) rebuilds only when that value changes. Consumer<Cart> scopes rebuilds to a small part of a widget.

A shopping cart model

ChangeNotifier exposed with provider.

class Cart extends ChangeNotifier {
  final List<Item> _items = [];
  List<Item> get items => List.unmodifiable(_items);
  int get count => _items.length;

  void add(Item item) {
    _items.add(item);
    notifyListeners();
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(create: (_) => Cart(), child: const MyApp()),
  );
}

class CartBadge extends StatelessWidget {
  const CartBadge({super.key});
  @override
  Widget build(BuildContext context) {
    final count = context.select<Cart, int>((cart) => cart.count);
    return Badge(label: Text('$count'), child: const Icon(Icons.shopping_cart));
  }
}

// in a button callback
onPressed: () => context.read<Cart>().add(item),

watch in build, read in callbacks

Calling context.watch inside onPressed throws or causes needless rebuilds; calling context.read in build means the widget will not update. Keep the rule simple.

Quick check: Which call should you use inside a button's onPressed to call a method on a provided model?

  • setState()
  • context.watch<T>()
  • context.read<T>()
  • Navigator.of(context)
Answer

context.read<T>() — read fetches the object without subscribing the widget to changes.