utils topic

utils

Helpers that ship with the package but belong to no scope family. They are exported because the scopes themselves are built on them and an application tends to want the same tools; nothing here needs a scope above it.

helper what it is
ListenableListenExtension.listen Stream-style subscription to a Listenable
ListenableSelectExtension.select a listener called only when a selected value changes
ListenableSelector the widget form of that selector
ListenableView a read-only façade over a Listenable
StateAsNotifier a mixin that makes a State listenable
ProgressIterator, Progress step counting for an initialization
ScreenshotReplacer freezes a subtree into the image it last painted
CompareUtils four comparison functions with names
IsBuildingExtension is a build running, and how to wait for the end of it

Listening to a Listenable

addListener and removeListener require keeping the callback around, which is why a closure cannot be unsubscribed without storing it somewhere. listen returns the subscription instead, the way Stream does:

final subscription = counter.listen(() => print(counter.value));
…
subscription.cancel();

CompositeListenableSubscription collects several of them and cancels the lot in one call.

select narrows the callback to one value:

final subscription = model.select(
  (model) => model.userName,
  (model, userName) => print(userName),
);

The listener runs only when the selected value changes. By default that means !=, and compare: replaces the test with one of your own. It answers the same question, so true means changed: notIdentical for a value that is replaced rather than mutated, a field-by-field comparison for a record, and so on. Passing identical there reports the opposite of what it is asked — the same object counts as a change and a replacement goes unnoticed. This is the same idea select uses on a scope, without a widget tree involved.

ListenableSelector is the widget wrapping the same mechanism:

ListenableSelector<Counter, int>(
  listenable: counter,
  selector: (counter) => counter.value,
  builder: (context, counter, value, child) => Text('$value'),
);

The child it takes is passed back to builder untouched, the standard way of keeping a subtree out of the rebuild.

ListenableView wraps a Listenable so that only addListener and removeListener are reachable — hand it out when the subtree may listen but must not notify. ScopeStateModelView in the ScopeNotifier topic is the same idea applied to a state model.

StateAsNotifier goes the other way: mix it into a State and the state itself becomes a Listenable, with a notifyListeners() for its own use. The ChangeNotifier behind it is created on the first listener and disposed of with the state, so a state nobody listens to costs nothing.

ProgressIterator

Step counting for an initialization that knows how many steps it has:

final progress = ProgressIterator(3);

progress.nextStep(); // 1/3
progress.nextStep(); // 2/3
progress.addSteps(1); // 3/3
progress.isCompleted; // true

Progress is the value it produces: number, total, value as a fraction between 0 and 1, and a toString of 2/3. The fraction holds those bounds whatever the pair says: a task of no steps at all reads as complete rather than as the NaN of 0 / 0, so it can go straight into a LinearProgressIndicator. Stepping past the total is a mistake in the caller and is caught by an assertion.

Reporting these from an initialization is the AsyncScope topic; ScopeAutoDependencies uses exactly this to report a step per dependency — see the Scope topic.

ScreenshotReplacer

Renders its child once, captures what was painted, and then shows the image in its place. The package uses it for close(): the last frame of a scope is frozen while the asynchronous disposal runs, so the closing screen is drawn over a still picture rather than over a subtree that is being torn down.

ScreenshotReplacer(
  onCompleted: () => print('captured, or given up on'),
  child: const HomeScreen(),
);

A subtree that is never painted — inside an Offstage, or in the unselected branch of an IndexedStack — cannot be captured. The attempt is therefore bounded by ScreenshotReplacer.maxRetries frames, after which onCompleted is called anyway — and the child is taken away all the same, with nothing in the picture's place. Nothing is reported: that is the case this widget documents as ordinary, and onCompleted together with the absence of a picture is how it is said. Giving up on the picture is not giving up on replacing the child: what waits on onCompleted waits in order to let go of whatever the child holds, and a child left standing gives that caller the report without the thing it was reported for. onCompleted fires exactly once per state, whichever way it ended, including when the widget is removed from the tree first.

The two small ones

CompareUtils is equals, notEquals, identical and notIdentical as named functions, for the places that take a comparison as a parameter — compare: above, among others — where a tear-off reads better than a lambda.

IsBuildingExtension extends SchedulerBinding. isBuilding says whether a build is running, and runOutsideFrame(action) runs the action now if it is safe, or in a post-frame callback if one is. This is what a scope uses to keep a notification out of the middle of a build.

A build is not only what a frame runs: runApp builds the first tree with no frame in progress, and marking an element dirty from inside that is refused just the same. isBuilding therefore adds two sources to the frame phase — the rebuilds of this package's own scopes, which it counts itself, and the build owner's flag, which it can only ask behind an assertion because that is the only place the flag exists. So the one case left unanswered is a build that neither belongs to a frame nor to this package, in a release build.

Where to go next

topic what it covers
ScopeNotifier the scope form of select, and the state models
Scope ProgressIterator in the dependency container, and close()
LiteScope close() and the screenshot in their own family

Classes

CompareUtils utils
CompositeListenableSubscription utils
Acts as a container for multiple subscriptions ListenableSubscription that can be canceled at once.
ListenableSelector<L extends Listenable, T extends Object?> utils
A widget that rebuilds when a value selected from a Listenable changes.
ListenableSelectSubscription<T extends Object?> utils
A subscription on selected value changes from a Listenable.
ListenableSubscription utils
A subscription on changes from a Listenable.
ListenableView<T extends Listenable> utils
Progress utils
ProgressIterator utils
A helper class to track initialization progress as an Progress value.
ScreenshotReplacer utils
A widget that renders its child once, captures a screenshot of it, and then replaces the child with the captured image.

Extensions

IsBuildingExtension on SchedulerBinding utils
ListenableListenExtension on Listenable utils
A helper extension that adds a listen method to Listenable, similar to Stream.listen. It returns a ListenableSubscription that can be easily canceled.
ListenableSelectExtension on L utils
A helper extension that adds a select method to Listenable. It allows listening to a specific value of the Listenable (selector), triggering the listener only when that value changes.