keep<T> method

  1. @nonVirtual
T keep<T>(
  1. Symbol name,
  2. T create(), {
  3. Object? id,
  4. List<Object?> keys = const [],
  5. void dispose(
    1. T value
    )?,
})

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;
}