Sharing state across screens with ChangeNotifier and Provider
Move state out of a single widget, notify listeners when it changes, and read it from any screen with watch and read.
setState works for one widget. The moment a cart count must appear on three screens, passing values through every constructor becomes painful and error-prone. Flutter screenings expect you to know at least one answer to this, and ChangeNotifier with the Provider package is the one most teams start with.
The model
A ChangeNotifier is a plain class that can tell listeners something changed. Keep the data private and expose read-only views so no widget can mutate it without a notification.
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 clear() {
_items.clear();
notifyListeners();
}
}Providing it above the app
Add provider under dependencies in pubspec.yaml and run flutter pub get. Then wrap the app so every route can reach the model. Placing the provider above MaterialApp matters: screens opened with Navigator are built under MaterialApp, so a provider inside one screen would not be visible to the next.
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartModel(),
child: const MyApp(),
),
);
}watch versus read
context.watch<T>() subscribes the widget, so it rebuilds whenever notifyListeners runs. Use it in build. context.read<T>() gets the model once without subscribing. Use it inside callbacks such as a button press.
class CartBadge extends StatelessWidget {
const CartBadge({super.key});
@override
Widget build(BuildContext context) {
final count = context.watch<CartModel>().count;
return Text('$count items');
}
}
class AddButton extends StatelessWidget {
const AddButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<CartModel>().add('Notebook'),
child: const Text('Add notebook'),
);
}
}Tap the button and only CartBadge rebuilds, because it is the only widget watching. AddButton used read, so it stays as it is. For a large screen where only one small part depends on the model, Consumer<CartModel> wraps just that part. Provider is a thin layer over InheritedWidget, which is the built-in mechanism; interviewers sometimes ask you to say so.
Common mistakes and a practice habit
- Calling
watchinside a callback. Flutter throws an assertion because listening outside build makes no sense. - Calling
readinbuildand wondering why the number never updates. - Changing the list and forgetting
notifyListeners(). - Exposing the raw list so another widget adds to it silently.
List.unmodifiableprevents this.
Practice habit: build a two-screen app where screen one adds items and screen two shows the count and a clear button. Confirm the count is correct after navigating back and forth, then replace one watch with Consumer and check the rebuild behaviour with the widget inspector.
More in Web and app