keep<T> method
Keeps whatever create returns alive across build passes, under name.
A pass re-runs from the top, which discards anything holding state.
Naming it runs create once and hands the same value back on every later
pass:
final square = keep(#square, () {
return ShapeNode(shape: .square(100));
});
square.paint.color = _COLOR;
Editing _COLOR repaints the existing square, because that line re-runs.
Editing the shape does nothing, because that closure does not.
Lookup is by name, not call position. Rename it to force a rebuild.
Pass keys to tie the value to something instead, and it is rebuilt
whenever they stop comparing equal:
final square = keep(#square, () {
return ShapeNode(shape: .square(_SIZE));
}, keys: [_SIZE]);
Pass id to name a collection one member at a time. A member arriving in
the data is built, one leaving is swept, and one that stays keeps
everything it accumulated:
final enemies = [
for (final spawn in level.spawns)
keep(#enemy, () => Enemy(spawn), id: spawn.id),
];
Ids have to be stable. Keying on a loop index puts every member after a removal onto the value in front of it.
Anything a pass stops declaring is dropped once that pass finishes: a
Node is detached, and dispose runs if it was given. Reach for
dispose rather than trash here, since the trash empties with every
pass and a kept value outlives them. A pass that throws part-way sweeps
nothing, so a node mid-edit keeps what it had.
Only valid inside this node's own build.
Implementation
@nonVirtual
T keep<T>(
Symbol name,
T Function() create, {
Object? id,
List<Object?> keys = const [],
void Function(T value)? dispose,
}) {
assert(
identical(Node._building, this),
'keep() is only available inside this node\'s own build.',
);
final key = (name, id);
final claimed = _claimed ??= {};
// Outside an assert, so the claim still lands in a release build.
final added = claimed.add(key);
assert(added, 'Two keep() declarations in $runtimeType share $key.');
final kept = _kept ??= {};
final shape = create.runtimeType;
final previous = kept[key];
// A changed shape or changed keys both retire the old value.
if (previous != null && previous.shape == shape && previous.matches(keys)) {
return previous.value as T;
}
previous?.discard();
final value = _construct(create);
Cleanup? cleanup;
if (dispose != null) cleanup = () => dispose(value);
kept[key] = _Kept(value, keys, cleanup, shape);
return value;
}