Shared state: ChangeNotifier and Provider

Flutter · Practical · 16 min

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 with context.watch<Cart>() (rebuilds on change) or context.read<Cart>() (no rebuild, for callbacks); context.select rebuilds 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/select to rebuild, read inside callbacks

Watch out for

  • Calling context.watch inside an onPressed callback — use read there.

Check yourself

  1. In provider, which call should an onPressed callback use to add an item to the cart?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.flutter.dev/data-and-backend/state-mgmt/simple