ScopeWidget topic

ScopeWidget

Two things live under this name. ScopeWidgetBase is the simplest scope of the package, usable as it is: a widget that hands its own parameters to its subtree. ScopeWidgetCore and ScopeWidgetElementBase are the pair every other family extends — the place where subscriptions are kept, where notifyDependents decides not to rebuild the subtree, and where a scope hooks into the element lifecycle.

Read the first section to use the family. Read the rest when writing a scope of your own, or when a scope rebuilt more — or less — than expected.

ScopeWidgetBase

The parameters of a widget are already immutable data provided by an ancestor; ScopeWidgetBase turns them into a scope so that descendants can subscribe to one parameter at a time instead of rebuilding on every change of the widget.

final class ApiConfig extends ScopeWidgetBase<ApiConfig> {
  final String apiKey;
  final Uri baseUrl;

  const ApiConfig({
    super.key,
    super.tag,
    required this.apiKey,
    required this.baseUrl,
    required super.child,
  });

  static String apiKeyOf(BuildContext context) =>
      ScopeWidgetBase.select<ApiConfig, String>(
        context,
        (widget) => widget.apiKey,
      );

  @override
  Widget build(BuildContext context) => child;
}

build is what the scope shows — the same role buildChild() plays for the other families. Returning the child the constructor took is the usual shape, which is why child is declared required here: the base class allows it to be omitted, and a scope that shows nothing is rarely what anyone wants.

Three accessors come from the base class, and all of them return the widget:

ScopeWidgetBase.of<ApiConfig>(context, listen: false);      // ApiConfig
ScopeWidgetBase.maybeOf<ApiConfig>(context, listen: false);  // ApiConfig?
ScopeWidgetBase.select<ApiConfig, String>(context, (w) => w.apiKey);

Re-exposing them as named statics, as apiKeyOf above, is worth the three lines: the call site stops repeating type arguments, and the scope decides what its subtree is allowed to read.

Where the values come from is the parent's business. When the parent rebuilds ApiConfig with a different apiKey, the dependents that selected apiKey are rebuilt and the ones that selected baseUrl are not — the filtering is described in the base topic.

The context handed to build is the scope's own element, so a lookup from it finds this very scope: ApiConfig.apiKeyOf(context) inside build subscribes the scope to itself. That is supported, not refused. InheritedElement does not let an element depend on itself, so the scope keeps such subscriptions of its own and honours them — filtered by their selector, like anybody else's. What it costs is the notify-only rebuild: when the dependent woken by a change is the scope, its whole subtree is rebuilt, where the same notification aimed at a leaf would have left that subtree standing.

In ScopeWidgetBase there is nothing to gain from it. What this family publishes are the scope's own parameters, and inside build they are already in hand — apiKey is a field, and build is a method of the same object. A subscription would pay a subtree rebuild for a value that was never out of reach; read the field.

The pair behind every family

final class MyScope extends ScopeWidgetCore<MyScope, MyScopeElement> {
  const MyScope({super.key, super.tag});

  @override
  MyScopeElement createScopeElement() => MyScopeElement(this);
}

final class MyScopeElement
    extends ScopeWidgetElementBase<MyScope, MyScopeElement> {
  MyScopeElement(super.widget);

  @override
  void init() {
    // Acquire whatever the scope owns…
    super.init();
  }

  @override
  void dispose() {
    super.dispose();
    // …and release it here.
  }

  @override
  Widget buildChild() => const SizedBox.shrink();
}

Both hooks are @mustCallSuper, and the order above is the one the package's own families keep: acquire before super.init(), release after super.dispose(). The two halves mirror each other, so whatever a layer owns outlives everything the layers under it set up.

init() runs after the element is connected to its ancestors and before its first buildChild. Resources acquired there, including a notifier listener, are therefore ready for the first subtree build.

It runs once. A hook that throws is not attempted again — running it twice would take a second copy of whatever the first attempt already held — and the scope shows an error instead of its subtree from then on. dispose() runs for that element too, so the half-finished attempt still gives back what it took: a disposer has to check what it actually owns rather than assume the whole hook ran, the way ScopeNotifier remembers whether it got as far as subscribing.

That is the whole contract: a widget that knows how to create its element, and an element that knows what to build. ScopeWidgetBase is exactly this, with buildChild() delegating to the build of the widget. Everything else the package offers — a model, a notifier, an asynchronous initialization, a dependency container — is added on top of the same two classes.

updateShouldNotify returns true unconditionally, which sounds wasteful and is not: it only means the element is asked to notify its dependents, and each dependent is then filtered by its own selectors. A scope has no way to know what its descendants care about, so the decision belongs to them.

The filtering is per build: what a dependent selected during its last build is what it depends on, and nothing else. A widget that reads user.name in one build and user.email in the next is not rebuilt by a later change to user.name, so a selector chosen by a condition is safe. The build boundary is taken from the frame, which leaves one case it cannot tell apart — a dependent rebuilt twice within a single frame keeps both sets of selectors, and pays at most one extra rebuild for it.

notifyDependents

notifyDependents() tells the subscribed descendants that something changed without rebuilding the subtree. It is what makes a scope affordable for values that change often:

@override
void init() {
  model.addListener(notifyDependents);
  super.init();
}

That is ScopeNotifier in full — every notifyListeners() of the model becomes a notification, and only the widgets whose selected value actually changed are rebuilt. The state of a Scope or a LiteScope calls the same method by hand after mutating itself.

The mechanism is worth knowing because it explains the constraints. A dependent is notified through didChangeDependencies, and that can only run while a frame is being built. So the element marks itself dirty, and on the resulting rebuild it notifies its clients and skips updating its child — the subtree keeps its elements, its states and its scroll positions untouched.

When the subtree is rebuilt anyway

Three cases override the notify-only path, and each is deliberate.

The parent updated the scope widget. A new widget can mean new parameters and a different buildChild(), so the subtree is rebuilt even if a notify-only rebuild was already pending.

The scope declared autoSelfDependence. An element that rebuilds its own subtree as its state advances cannot use the shortcut, because its buildChild returns a different branch each time. AsyncScope sets it for good. LiteScope is more precise: it starts with the flag on, while the waiting, initializing and error branches replace one another, and clears it in buildOnReady() — from the moment the ready branch is on screen, notifications stop rebuilding the subtree.

A self-dependency fired. An element may subscribe to its own scope; InheritedElement refuses to record that (an assert in notifyClients), so those subscriptions are kept in a separate list and notified before the others. When one of them changes, the subtree is rebuilt — the element that selected the value is the element that builds the subtree.

Names in the output

toStringShort of a scope widget is MyScope(#4e0b7), or MyScope(cart) when a tag was given; the element prints its own type and hash. A scope reports itself to ScopeConfig.observer under the first — that is its debugLabel — which is why a tag is the cheapest way to tell two scopes of the same type apart in the output. The debug topic has the format.

Where to go next

topic what it covers
base the lookup protocol these classes implement: of, select, listen
ScopeModel, ScopeNotifier the first two families built on this pair
Scope the full family, and what notifyDependents means for a state
debug the observer, and what a tag does to it

Classes

ScopeWidgetAccess<W extends ScopeWidgetBase<W>> ScopeWidget
The three accessors of one ScopeWidgetBase, with its type argument named once.
ScopeWidgetBase<W extends ScopeWidgetBase<W>> ScopeWidget
ScopeWidgetCore<W extends ScopeWidgetCore<W, E>, E extends ScopeWidgetElementBase<W, E>> ScopeWidget
ScopeWidgetElementBase<W extends ScopeWidgetCore<W, E>, E extends ScopeWidgetElementBase<W, E>> ScopeWidget