ScopeNotifier topic
ScopeNotifier
ScopeModel for a Listenable. Everything from that topic holds — create
and dispose, the .value constructor, the three levels of the family — with
one addition that changes how the scope is used: the element subscribes to the
model, so a notifyListeners() reaches the subtree without anything above the
scope being rebuilt.
ScopeNotifier<Counter>(
create: (context) => Counter(),
dispose: (counter) => counter.dispose(),
builder: (context) => const CounterText(),
);
class CounterText extends StatelessWidget {
const CounterText({super.key});
@override
Widget build(BuildContext context) {
final value = ScopeNotifier.select<Counter, int>(
context,
(counter) => counter.value,
);
return TextButton(
onPressed: ScopeNotifier.of<Counter>(context, listen: false).increment,
child: Text('$value'),
);
}
}
Pressing the button mutates the counter, the counter notifies its listeners,
the scope notifies its dependents, and CounterText is rebuilt — but only
because the value it selected changed. A sibling that selected something else
sleeps through it, and the subtree between the scope and the button is not
rebuilt at all.
The whole difference
@override
void init() {
model.addListener(notifyDependents);
super.init();
}
@override
void dispose() {
super.dispose();
model.removeListener(notifyDependents);
}
That is the entire element. notifyDependents is described in the
ScopeWidget topic; what matters here is that it does not rebuild the scope's
own subtree, which is what makes a high-frequency Listenable — an animation,
a scroll position, a text controller — affordable to put in a scope.
The subscription belongs to the element, so it lasts exactly as long as the
model does, and a model created by create is disposed of after the listener
has been removed.
Swapping the model
The .value constructor takes a Listenable somebody else owns, and it may be
handed a different one on a later build:
ScopeNotifier.value(value: currentPlayer, builder: ...)
When the widget is rebuilt with another value, the element moves its listener
from the old model to the new one. "Another" means another object, not another
value: two models that compare == are still two listener lists, so the move is
decided by identity. Descendants then see the new model through the same
accessors, and their selectors compare against the values they captured from
the old one — so a switch to a model with different values rebuilds exactly the
widgets those values differ for.
As with ScopeModel.value, the scope does not own a model given this way: no
dispose is called for it.
What cannot change is which constructor the scope was built with. .value and
the owning constructor are two different answers to "who releases this model",
and the answer is fixed for the lifetime of the element — an assertion refuses
a rebuild that changes it. To switch, give the widget a different Widget.key:
the framework then builds a new element, which reads the mode afresh and
releases whatever the old one owned.
State models
Six types in this family exist for scopes whose state is a single immutable
value that changes over time — a trio, and the same trio with a failed state
beside the value. AsyncScope is built on the first three; the second three are
offered for a family of your own:
| type | what it is |
|---|---|
ScopeStateModel<S> |
a Listenable with a state of type S — the read side |
ScopeStateNotifier<S> |
a ChangeNotifier implementing it, with update(S) |
ScopeStateModelView<S> |
an unmodifiable view of a notifier |
ScopeStateWithErrorModel<S> |
the read side, plus a failed state |
ScopeStateWithErrorNotifier<S> |
its notifier, with update(S) and setError |
ScopeStateWithErrorModelView<S> |
an unmodifiable view of that notifier |
update(S value) notifies only when the value actually changed, and what
"changed" means is a method you can override:
base class PlayerState extends ScopeStateNotifier<Player> {
PlayerState(super.initialState);
@override
bool shouldNotify(Player previous, Player current) =>
previous.id != current.id;
}
The default shouldNotify returns true — every update notifies. That is
the safe default for a mutable object being re-assigned; override it when the
state is a value type and repeated equal updates are common.
asUnmodifiable() wraps a notifier into a ScopeStateModelView, which forwards
state, addListener and removeListener and nothing else. Hand that to the
subtree when the state must be readable but not settable from below.
The error-carrying pair is the interesting one, and nothing in the package
uses it. setError stores the error with its stack trace, hasError reports
it — and reading state afterwards rethrows that error with its original
stack trace instead of returning a value, so a builder that reads state
fails loudly on a scope that has failed rather than rendering a stale value.
A failure is not terminal, though: update puts it down as it stores the new
state. A state handed over is a state that can be read, so recovering is what
an update after setError means, and there is nothing else it could mean. The
listeners hear about it even when the value is the one from before the failure
— shouldNotify weighs one value against another, and this change is between
a state that throws and one that does not.
model
..setError(failure, stackTrace) // `state` throws from here on
..update(recovered); // and stops: `hasError` is false again
That is not how the asynchronous families model a failure. They hold an
ordinary ScopeStateNotifier and put the failure in the state, as an
AsyncScopeError beside AsyncScopeWaiting, AsyncScopeProgress and
AsyncScopeReady: reading AsyncScope.of(context, listen: true).state never
throws, and hasError is derived from what the state is. Both shapes work;
which one to pick depends on whether a failed scope still has something to
show.
Where to go next
| topic | what it covers |
|---|---|
ScopeModel |
the family this one extends: create, dispose, .value, lifetime |
ScopeWidget |
notifyDependents and why it skips the subtree |
base |
of, select, listen |
AsyncScope |
a family built on ScopeStateNotifier, with the failure inside the state |
Classes
-
ScopeNotifier<
M extends Listenable> ScopeNotifier -
ScopeNotifierAccess<
W extends ScopeNotifierBase< ScopeNotifierW, M> , M extends Listenable> - The three accessors of one ScopeNotifierBase, with its type arguments named once.
-
ScopeNotifierBase<
W extends ScopeNotifierBase< ScopeNotifierW, M> , M extends Listenable> -
ScopeNotifierCore<
W extends ScopeNotifierCore< ScopeNotifierW, E, M> , E extends ScopeNotifierElementBase<W, E, M> , M extends Listenable> -
ScopeNotifierElementBase<
W extends ScopeModelCore< ScopeNotifierW, E, M> , E extends ScopeNotifierElementBase<W, E, M> , M extends Listenable> -
ScopeStateModel<
S extends Object> ScopeNotifier -
ScopeStateModelView<
S extends Object> ScopeNotifier -
ScopeStateNotifier<
S extends Object> ScopeNotifier -
ScopeStateWithErrorModel<
S extends Object> ScopeNotifier -
ScopeStateWithErrorModelView<
S extends Object> ScopeNotifier -
ScopeStateWithErrorNotifier<
S extends Object> ScopeNotifier