State Management
Why setState isn’t enough
setState works great for state inside a single widget. But real apps share data across many screens — a logged-in user, a shopping cart, a theme. Passing that data down through many widget constructors (“prop drilling”) becomes painful. State management solutions solve this.
The options, simply
- setState — local state in one widget. Always your first choice when it fits.
- Provider — simple, popular, officially backed. Great starting point for shared state.
- Riverpod — a modern, compile-safe evolution of Provider; testable and not tied to the widget tree.
- BLoC — an event/stream-based pattern for large, complex apps.
Provider in practice
You create a class that holds state and notifies listeners when it changes, provide it above your widgets, and read it where needed.
// 1. the model
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
void add(String item) {
_items.add(item);
notifyListeners(); // rebuild widgets that watch this
}
}
// 2. provide it near the top
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartModel(),
child: const MyApp(),
),
);
}
// 3. read/watch it anywhere below
final cart = context.watch<CartModel>(); // rebuilds on change
Text('${cart.items.length} items');
context.read<CartModel>().add('Shoes'); // call a method (no rebuild)
watch rebuilds the widget when the data changes; read is for one-off actions (like calling add) without subscribing.
Riverpod (a quick taste)
Riverpod removes the need to access state through the widget tree and is fully type-safe:
final counterProvider = StateProvider<int>((ref) => 0);
// in a ConsumerWidget
final count = ref.watch(counterProvider);
ref.read(counterProvider.notifier).state++;
How to choose
- State used by one widget →
setState. - State shared across a few screens → Provider or Riverpod.
- Large app with complex flows/events → Riverpod or BLoC.
Don’t over-engineer: start with setState, and reach for a solution only when sharing state becomes awkward.
Common mistakes
- Forgetting
notifyListeners()so the UI never updates. - Using
watchinside a callback (usereadthere). - Jumping to a heavy solution (BLoC) before you actually need it.
Summary: UsesetStatefor local state; for shared state use Provider (ChangeNotifier+notifyListeners) or Riverpod.watchto rebuild on changes,readfor actions. Start simple and scale up only when needed.