skip to content

With Flutter's provider package, how do ProxyProvider and ChangeNotifierProxyProvider build a value from other providers, and what goes wrong if update creates a new notifier?

level: seniorimportance: should knowfreq 36%

answer

  1. derived from providers above
  2. update reruns when a dependency changes
  3. previous is the last value
  4. ProxyProvider2 to 6 for more inputs
  5. reuse previous, never re-create in update

basics

~20 s

ProxyProvider builds a value from providers above it in update, which runs on first read and again whenever a dependency changes. ChangeNotifierProxyProvider creates a notifier once and updates it; returning a new notifier from update loses its state and disposes the previous one.

solid answer

~50 s

`ProxyProvider<A, R>` reads `A` from above and builds `R` in `update(context, a, previous)`. `update` runs when the value is first read and again whenever the proxy rebuilds or a provider it depends on changes; `previous` is the last value, `null` the first time. That fits derived, immutable values: a bakery app's `ProxyProvider2<Cart, PriceList, CartTotal>` recomputes the total whenever the cart notifies. The digit, from `ProxyProvider2` to `ProxyProvider6`, is the number of inputs. For a mutable object use `ChangeNotifierProxyProvider`: `create` builds the notifier once and `update` passes new inputs into `previous`, for example `checkout!..total = total`. If `update` returns a new `Checkout(total)` instead, every change discards the old notifier's state, and because the value differs, the provider disposes the previous notifier and resubscribes. The package advises preferring plain `ProxyProvider` when the result is a pure combination. Order matters: the proxy must sit below its inputs.

code

dart · 66 lines
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class PriceList {
  const PriceList(this.cents);
  final Map<String, int> cents;
}

class Cart extends ChangeNotifier {
  final Map<String, int> _quantities = {};
  Map<String, int> get quantities => Map.unmodifiable(_quantities);

  void add(String item) {
    _quantities.update(item, (q) => q + 1, ifAbsent: () => 1);
    notifyListeners();
  }
}

class CartTotal {
  const CartTotal(this.cents);
  final int cents;

  factory CartTotal.of(Cart cart, PriceList prices) {
    var sum = 0;
    cart.quantities.forEach((item, qty) => sum += (prices.cents[item] ?? 0) * qty);
    return CartTotal(sum);
  }

  @override
  bool operator ==(Object other) => other is CartTotal && other.cents == cents;

  @override
  int get hashCode => cents.hashCode;
}

class Checkout extends ChangeNotifier {
  CartTotal total = const CartTotal(0);
  bool placing = false;

  Future<void> placeOrder() async {
    placing = true;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 1)); // uses total
    placing = false;
    notifyListeners();
  }
}

final bakeryProviders = MultiProvider(
  providers: [
    Provider<PriceList>(
      create: (_) => const PriceList({'Baguette': 350, 'Eclair': 420}),
    ),
    ChangeNotifierProvider<Cart>(create: (_) => Cart()),
    // Immutable, derived: recomputed whenever Cart notifies.
    ProxyProvider2<Cart, PriceList, CartTotal>(
      update: (_, cart, prices, _) => CartTotal.of(cart, prices),
    ),
    // Mutable, long-lived: created once, fed the latest total.
    ChangeNotifierProxyProvider<CartTotal, Checkout>(
      create: (_) => Checkout(),
      update: (_, total, checkout) => checkout!..total = total,
    ),
  ],
  child: const SizedBox.shrink(),
);

go deeper

for a junior

Recall that ProxyProvider builds a value from other providers and updates it when they change, and that inputs must be listed above it.

for a middle

Explain when update runs, what previous holds, how the numbered variants work, and why derived results should implement ==.

for a senior

Keep proxy chains cheap and correct: immutable ProxyProvider results, ChangeNotifierProxyProvider that mutates previous, and no re-creation that disposes live notifiers.

for a principal

Weigh proxy chains against restructuring state so dependencies stay shallow and understandable as the provider graph grows across teams.

## The problem proxies solve A provider's `create` runs once. If an object depends on another provider's value that can change — a cart total that depends on the cart, a checkout that needs the current total — `create` would capture the first value and never see later ones. The package's documentation spells this out and points to the **proxy providers**, whose `update` callback runs again when an input changes. ## `ProxyProvider`: derived values `ProxyProvider<T, R>` reads a `T` from an ancestor provider and exposes an `R`. Its callback has the signature `update(BuildContext context, T value, R? previous)`: - `update` is called when the value is **first read**, like `create` in other providers; - it is called again **whenever the proxy rebuilds or a provider it depends on updates**, because under the hood the proxy reads its inputs with a listening `Provider.of`; - `previous` is the last value returned, `null` the first time. The number after the class name is the number of inputs: `ProxyProvider2<Cart, PriceList, CartTotal>` receives both, up to `ProxyProvider6`. `ProxyProvider0` has no typed inputs and reads what it needs inside `update`. When `update` returns a value that is not `==` to the previous one, dependents rebuild; if a `dispose` callback was given, the previous value is disposed. For a **pure combination** of inputs — a total computed from quantities and prices — `ProxyProvider` with an immutable result is the recommended tool. ## `ChangeNotifierProxyProvider`: long-lived notifiers with inputs Sometimes the result has its own mutable state and methods, such as a `Checkout` notifier that tracks whether an order is being placed. `ChangeNotifierProxyProvider<T, R>` requires both callbacks: 1. `create` builds the notifier **once**; 2. `update(context, value, previous)` hands the new input to that existing notifier, typically through a setter or method, and returns it. It listens to the notifier like `ChangeNotifierProvider` and disposes it when leaving the tree. ## The mistake: re-creating in `update` The package lists this as a **don't**: returning `Checkout(total)` from `update`. | Behaviour | Reusing `previous` | Creating a new notifier in `update` | |---|---|---| | State such as `isPlacingOrder` | kept | lost on every input change | | Previous notifier | kept and still listened to | disposed, listener removed | | Subscription | unchanged | re-subscribed to the new object | | Dependents | rebuild only when the notifier notifies | rebuild because the value changed identity | In the source, when `update` returns a value that differs from the previous one, the provider removes its listener from the old value and calls the dispose callback on it, which for the ChangeNotifier variant is `dispose()`. A widget still holding the old notifier would then be calling a disposed object. ## Ordering and composition Proxy providers find their inputs by walking **up** the tree, so inputs must be listed **earlier** in a `MultiProvider`: ```dart MultiProvider( providers: [ Provider<PriceList>(create: (_) => const PriceList({'Baguette': 350})), ChangeNotifierProvider<Cart>(create: (_) => Cart()), ProxyProvider2<Cart, PriceList, CartTotal>( update: (_, cart, prices, _) => CartTotal.of(cart, prices), ), ], child: const BakeryApp(), ) ``` Chains are possible — a proxy can depend on another proxy — but each link recomputes when anything upstream changes, so long chains should stay cheap and their values should implement `==` to avoid needless rebuilds. ## Proxy or computed getter? Not every derived value needs its own provider. If only one widget needs the cart total, a getter on `Cart` — `int totalWith(PriceList prices)` — or a computation in that widget's `build` is simpler. A proxy earns its place when several widgets across the tree need the derived value, when it should be looked up by type like any other dependency, or when its `==` lets unrelated cart changes skip rebuilds of widgets that only show the total. The same reasoning applies in reverse: a proxy whose result nobody reads by type is indirection without benefit. ## Checklist - Immutable derived value: `ProxyProvider`, with `==` on the result. - Mutable object with its own state: `ChangeNotifierProxyProvider`, create once, mutate `previous` in `update`. - Inputs above the proxy, never below. - Keep `update` fast and free of network calls; start those from the notifier's methods.

  • Why should a ProxyProvider's result implement ==?
    Because the provider compares the value returned by `update` with the previous one to decide whether dependents rebuild. Without `==`, every recomputation produces a new object that is never equal to the last, so every widget reading `CartTotal` rebuilds whenever the cart notifies, even when the total did not change.
  • Why is checkout! safe in ChangeNotifierProxyProvider's update?
    Because `create` is required on `ChangeNotifierProxyProvider` and runs before the first `update`, so `previous` already holds the created notifier. In plain `ProxyProvider`, `create` is optional and `previous` is null on the first call, so the callback must handle that case.
  • What does the digit in ProxyProvider2 or ProxyProvider6 mean?
    The number of providers the proxy reads. `ProxyProvider<A, R>` takes one input, `ProxyProvider2<A, B, R>` two, up to six. `ProxyProvider0<R>` has none typed and reads whatever it needs inside `update`. The ChangeNotifier and Listenable proxy families follow the same numbering.

saying these in an interview costs you the question

  • ProxyProvider's update runs only once, like create.
  • Returning a new notifier from update is harmless because the old one is garbage collected.
  • A proxy can depend on providers declared after it in MultiProvider.
  • previous is always non-null in plain ProxyProvider.
  • ChangeNotifierProxyProvider is preferred even for pure derived values.