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
Listenablechanges. -
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.
Mixins
-
StateAsNotifier<
T extends StatefulWidget> utils
Extensions
- IsBuildingExtension on SchedulerBinding utils
- ListenableListenExtension on Listenable utils
-
A helper extension that adds a
listenmethod toListenable, similar toStream.listen. It returns aListenableSubscriptionthat 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.