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?
answer
- derived from providers above
- update reruns when a dependency changes
- previous is the last value
- ProxyProvider2 to 6 for more inputs
- reuse previous, never re-create in update
basics
~20 sProxyProvider 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 linesimport '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
Recall that ProxyProvider builds a value from other providers and updates it when they change, and that inputs must be listed above it.
Explain when update runs, what previous holds, how the numbered variants work, and why derived results should implement ==.
Keep proxy chains cheap and correct: immutable ProxyProvider results, ChangeNotifierProxyProvider that mutates previous, and no re-creation that disposes live notifiers.
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.