← All courses

State Management

🗓 May 31, 2026 ⏱ 2 min read

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 watch inside a callback (use read there).
  • Jumping to a heavy solution (BLoC) before you actually need it.
Summary: Use setState for local state; for shared state use Provider (ChangeNotifier + notifyListeners) or Riverpod. watch to rebuild on changes, read for actions. Start simple and scale up only when needed.