Kodokon kodokon.com

Structured state management: ChangeNotifier and Provider

Share application state with ChangeNotifier and Provider while mastering watch, read, and select for targeted rebuilds.

10 min · 3 questions

Open this lesson in Kodokon

setState is enough as long as state lives and dies with a single screen. The moment data is shared - cart, session, preferences - passing it down from constructor to constructor becomes unmanageable. ChangeNotifier provides the SDK's minimal observable; provider injects it into the tree and rebuilds only the subscribed widgets. Compared to Riverpod or Bloc, this pairing remains the easiest to audit: little magic, a single dependency, a mechanism you can explain in one sentence.

BASH
flutter pub add provider

A well-designed notifier encapsulates: private fields, read-only getters, mutations only through methods that end with notifyListeners(). That contract is what guarantees no state change slips past the UI.

DART
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 remove(String item) {
    _items.remove(item);
    notifyListeners();
  }
}
Private state, read-only to the outside, notification.

Three entry points, three uses. context.watch<T>() subscribes the widget: it rebuilds on every notifyListeners. context.read<T>() reads without subscribing: it's the tool for callbacks like onPressed. context.select<T, R>() subscribes only to a projection: the widget rebuilds only if that precise value changes. Golden rule: watch and select in build only, read in handlers - never the reverse.

DART
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CounterModel extends ChangeNotifier {
  int _value = 0;

  int get value => _value;

  void increment() {
    _value++;
    notifyListeners();
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: const CounterApp(),
    ),
  );
}

class CounterApp extends StatelessWidget {
  const CounterApp({super.key});

  @override
  Widget build(BuildContext context) {
    final value = context.watch<CounterModel>().value;
    return MaterialApp(
      home: Scaffold(
        body: Center(child: Text('Count: $value')),
        floatingActionButton: FloatingActionButton(
          onPressed: () =>
              context.read<CounterModel>().increment(),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
watch in build to read, read in the callback to act.
DART
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  int get count => _items.length;
}

class CartBadge extends StatelessWidget {
  const CartBadge({super.key});

  @override
  Widget build(BuildContext context) {
    final count = context.select(
      (CartModel cart) => cart.count,
    );
    return Badge(
      label: Text('$count'),
      child: const Icon(Icons.shopping_cart),
    );
  }
}
CartBadge only rebuilds when count changes.

Knowledge check

Make sure you remember the key points of this lesson.

  1. Inside an onPressed, how do you trigger cart.add(...) without creating a needless subscription?
    • context.watch<CartModel>().add(...)
    • context.read<CartModel>().add(...)
    • context.select((CartModel c) => c.add(...))
  2. What exactly does notifyListeners() trigger?
    • The rebuild of only the widgets that use the changed data.
    • A complete rebuild of the application.
    • The rebuild of all widgets subscribed to the notifier, regardless of which data changed.
  3. Why expose List.unmodifiable(_items) rather than _items directly?
    • To improve the read performance of the list.
    • To prevent an external mutation that would change the state without notifyListeners, and therefore without a rebuild.
    • Because Provider refuses to inject mutable lists.