Once several screens need the same data, setState alone becomes unmanageable.
setState is ideal for ephemeral state used by one widget. App state — the signed-in user, a cart, settings — is needed by many widgets across screens.
Step one is lifting state up: move it to the closest common ancestor and pass data and callbacks down. This works but leads to "prop drilling" through many layers.
Flutter's built-in solution for passing data down is `InheritedWidget`: descendants call context.dependOnInheritedWidgetOfExactType<T>() (usually wrapped in a static of(context)), and they're rebuilt automatically when it changes. Theme.of and MediaQuery.of work this way.
To model changing state, use a `ChangeNotifier`: a class that holds data, exposes methods that mutate it and calls notifyListeners(). Widgets rebuild via ListenableBuilder, or through packages:
- provider — wraps InheritedWidget. Provide with
ChangeNotifierProvider(create: (_) => Cart(), child: ...); read withcontext.watch<Cart>()(rebuilds on change) orcontext.read<Cart>()(no rebuild, for callbacks);context.selectrebuilds only when a chosen value changes. - Riverpod, Bloc/Cubit, signals and others offer different trade-offs in testability and boilerplate.
The official architecture guide uses ChangeNotifier view models with ListenableBuilder. Whatever you pick, keep business logic out of widgets, keep state immutable where practical, and rebuild as small a subtree as possible.
Example
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Cart extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
void add(String item) { _items.add(item); notifyListeners(); }
}
void main() => runApp(ChangeNotifierProvider(
create: (_) => Cart(),
child: const MaterialApp(home: Shop()),
));
class Shop extends StatelessWidget {
const Shop({super.key});
@override Widget build(BuildContext context) => Scaffold(
appBar: AppBar(actions: const [CartBadge()]),
body: Center(child: FilledButton(
onPressed: () => context.read<Cart>().add('Tea'), // no rebuild here
child: const Text('Add tea'))),
);
}
class CartBadge extends StatelessWidget {
const CartBadge({super.key});
@override Widget build(BuildContext context) => Badge( // rebuilds only when the count changes
label: Text('${context.select((Cart c) => c.items.length)}'), child: const Icon(Icons.shopping_cart));
}Remember
- Ephemeral state: setState; shared app state: lift up or use a notifier
- ChangeNotifier + notifyListeners models observable state
- provider:
watch/selectto rebuild,readinside callbacks
Watch out for
- Calling
context.watchinside anonPressedcallback — usereadthere.