MagicSelector<C extends MagicController, T> class

Rebuilds one subtree when one part of a controller changes, and leaves it alone the rest of the time.

MagicController.refreshUI notifies every listener, and MagicStatefulViewState answers by calling setState on the whole view. That is the right default: a controller does not know which of its fields a screen reads, and a view that rebuilds is always correct. It stops being cheap on a screen where one field changes often and most of the screen does not care. A search field is the worked example: every keystroke is a notification, and a consumer measured one keystroke rebuilding 220 styled containers, almost none of which could have looked different.

MagicSelector<GuideController, String>(
  controller: controller,
  selector: (GuideController c) => c.countLabel,
  builder: (String label) => WText(label),
)

How it avoids the rebuild

It caches the widget the builder returned and, while the selected value compares equal, returns that same INSTANCE. Element.updateChild short circuits when the new widget is == to the mounted one, so an identical instance ends the descent right there and the subtree is never visited. That is what makes this work under a parent that rebuilds anyway: a widget that merely skipped its own setState would still be rebuilt from above.

The contract this buys

builder must be a pure function of the value it is handed. A cached child cannot see anything else the closure captured, so this is stale for as long as count happens not to move:

// WRONG: `total` is captured, and nothing here watches it.
MagicSelector<C, int>(
  controller: c,
  selector: (C c) => c.count,
  builder: (int count) => WText('$count of $total'),
)

Select both instead. A Dart record has value equality, so it compares by content and the cache still holds:

MagicSelector<C, (int, int)>(
  controller: c,
  selector: (C c) => (c.count, c.total),
  builder: ((int, int) v) => WText('${v.$1} of ${v.$2}'),
)

Reading an InheritedWidget INSIDE the cached subtree is fine and needs no selection: Theme.of, MediaQuery.of and WindTheme.of register their own dependency, and the framework rebuilds a dependent element directly rather than through its parent.

A lookup captured from the ENCLOSING build is the same hole as total above, and a dark-mode toggle is a likelier way to meet it:

// WRONG: `context` is the view's, so a theme change rebuilds the view, the
// cache is served, and this subtree keeps the old theme.
builder: (int n) => WDiv(className: WindTheme.of(context).surface),

// Right: the lookup runs inside the built subtree.
builder: (int n) => Builder(
  builder: (BuildContext inner) =>
      WDiv(className: WindTheme.of(inner).surface),
),

Equality

Plain ==, deliberately. A selector that returns a freshly built List or Map therefore never matches its own cache, because Dart gives collections identity equality, and the subtree rebuilds every notification exactly as it would have without this widget. Deep comparison was the alternative and is worse where it matters: walking a ten thousand channel list on every keystroke costs more than the rebuild it prevents. Select a scalar, a record, or an object whose identity is stable across notifications.

See also:

Inheritance

Constructors

MagicSelector({Key? key, required C controller, required T selector(C controller), required Widget builder(T value)})
Creates a MagicSelector.
const

Properties

builder Widget Function(T value)
Builds the subtree from the selected value, and from nothing else.
final
controller → C
The controller to watch.
final
hashCode int
The hash code for this object.
no setterinherited
key Key?
Controls how one widget replaces another widget in the tree.
finalinherited
runtimeType Type
A representation of the runtime type of the object.
no setterinherited
selector → T Function(C controller)
Reads the one piece of controller this subtree depends on.
final

Methods

createElement() StatefulElement
Creates a StatefulElement to manage this widget's location in the tree.
inherited
createState() State<MagicSelector<C, T>>
Creates the mutable state for this widget at a given location in the tree.
override
debugDescribeChildren() List<DiagnosticsNode>
Returns a list of DiagnosticsNode objects describing this node's children.
inherited
debugFillProperties(DiagnosticPropertiesBuilder properties) → void
Add additional properties associated with the node.
inherited
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
toDiagnosticsNode({String? name, DiagnosticsTreeStyle? style}) DiagnosticsNode
Returns a debug representation of the object that is used by debugging tools and by DiagnosticsNode.toStringDeep.
inherited
toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) String
A string representation of this object.
inherited
toStringDeep({String prefixLineOne = '', String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug, int wrapWidth = 65}) String
Returns a string representation of this node and its descendants.
inherited
toStringShallow({String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug}) String
Returns a one-line detailed description of the object.
inherited
toStringShort() String
A short, textual description of this widget.
inherited

Operators

operator ==(Object other) bool
The equality operator.
inherited