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:
- MagicBuilder, for a plain ValueListenable with no selection step.
- Inheritance
-
- Object
- DiagnosticableTree
- Widget
- StatefulWidget
- MagicSelector
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