Share application state with ChangeNotifier and Provider while mastering watch, read, and select for targeted rebuilds.
Open this lesson in KodokonsetState is enough as long as state lives and dies with a single screen. The moment data is shared - cart, session, preferences - passing it down from constructor to constructor becomes unmanageable. ChangeNotifier provides the SDK's minimal observable; provider injects it into the tree and rebuilds only the subscribed widgets. Compared to Riverpod or Bloc, this pairing remains the easiest to audit: little magic, a single dependency, a mechanism you can explain in one sentence.
flutter pub add providerA well-designed notifier encapsulates: private fields, read-only getters, mutations only through methods that end with notifyListeners(). That contract is what guarantees no state change slips past the UI.
import 'package:flutter/foundation.dart';
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
int get count => _items.length;
void add(String item) {
_items.add(item);
notifyListeners();
}
void remove(String item) {
_items.remove(item);
notifyListeners();
}
}Three entry points, three uses. context.watch<T>() subscribes the widget: it rebuilds on every notifyListeners. context.read<T>() reads without subscribing: it's the tool for callbacks like onPressed. context.select<T, R>() subscribes only to a projection: the widget rebuilds only if that precise value changes. Golden rule: watch and select in build only, read in handlers - never the reverse.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CounterModel extends ChangeNotifier {
int _value = 0;
int get value => _value;
void increment() {
_value++;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const CounterApp(),
),
);
}
class CounterApp extends StatelessWidget {
const CounterApp({super.key});
@override
Widget build(BuildContext context) {
final value = context.watch<CounterModel>().value;
return MaterialApp(
home: Scaffold(
body: Center(child: Text('Count: $value')),
floatingActionButton: FloatingActionButton(
onPressed: () =>
context.read<CounterModel>().increment(),
child: const Icon(Icons.add),
),
),
);
}
}import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CartModel extends ChangeNotifier {
final List<String> _items = [];
int get count => _items.length;
}
class CartBadge extends StatelessWidget {
const CartBadge({super.key});
@override
Widget build(BuildContext context) {
final count = context.select(
(CartModel cart) => cart.count,
);
return Badge(
label: Text('$count'),
child: const Icon(Icons.shopping_cart),
);
}
}
onPressed, how do you trigger cart.add(...) without creating a needless subscription?context.watch<CartModel>().add(...)context.read<CartModel>().add(...)context.select((CartModel c) => c.add(...))notifyListeners() trigger?List.unmodifiable(_items) rather than _items directly?notifyListeners, and therefore without a rebuild.