syncMultipleChildren method

void syncMultipleChildren(
  1. Map<TKey, List<TreeNode<TKey, TData>>> desiredByParent, {
  2. bool animate = true,
})

Syncs children for multiple parents in a single batch.

This is the safe way to reparent nodes across parents when calling syncChildren directly (outside of syncRoots). The method pre-computes the union of all desired child keys so that removal of a node from its old parent is deferred when it is desired under a different parent, allowing TreeController.moveNode to preserve subtree state.

The same child key MUST NOT appear under two different parents in desiredByParent. If it does, the second syncChildren call would reparent the key out of the first parent's tree, producing last-write-wins semantics. Debug builds assert against this; release builds silently apply last-write-wins.

Set animate to false to suppress animations.

Implementation

void syncMultipleChildren(
  Map<TKey, List<TreeNode<TKey, TData>>> desiredByParent, {
  bool animate = true,
}) {
  _controller.runBatch(() {
    _globallyDesiredChildren = <TKey>{};
    try {
      // Build the union of all desired keys AND detect cross-parent
      // duplicates in debug mode. The duplicate-key check costs one
      // extra Set lookup per child but only runs under `assert(...)`.
      assert(() {
        final seenKeys = <TKey>{};
        for (final entry in desiredByParent.entries) {
          for (final c in entry.value) {
            if (!seenKeys.add(c.key)) {
              throw FlutterError(
                "syncMultipleChildren: child key ${c.key} appears under "
                "more than one parent in desiredByParent. The same key "
                "cannot be a child of two parents; the second "
                "syncChildren call would reparent it (last-write-wins). "
                "Deduplicate the input map before calling.",
              );
            }
          }
        }
        return true;
      }());
      for (final children in desiredByParent.values) {
        for (final c in children) {
          _globallyDesiredChildren!.add(c.key);
        }
      }
      _deferredSubtreeRemovals = <TKey>[];
      // See [_moverAncestors]: one upward pass per mover, rather than
      // a downward subtree scan per removal candidate.
      final moverAncestors = _moverAncestors = <TKey>{};
      for (final moverKey in _globallyDesiredChildren!) {
        if (_controller.getNodeData(moverKey) == null) {
          continue;
        }
        var cursor = _controller.getParent(moverKey);
        while (cursor != null && moverAncestors.add(cursor)) {
          cursor = _controller.getParent(cursor);
        }
      }
      for (final entry in desiredByParent.entries) {
        syncChildren(entry.key, entry.value, animate: animate);
      }
      // Every mover has been placed by its destination parent's sync,
      // so what is left under a deferred key is the remainder nobody
      // asked for. Skip a key that is already gone, that has since
      // moved somewhere this call does not describe, or that turned out
      // to be desired under its own current parent after all.
      for (final key in _deferredSubtreeRemovals!) {
        if (_controller.getNodeData(key) == null) {
          continue;
        }
        final parent = _controller.getParent(key);
        if (parent == null || !desiredByParent.containsKey(parent)) {
          continue;
        }
        if (desiredByParent[parent]!.any((node) => node.key == key)) {
          continue;
        }
        _rememberExpansion(key);
        _controller.remove(key: key, animate: animate);
      }
    } finally {
      _globallyDesiredChildren = null;
      _deferredSubtreeRemovals = null;
      _moverAncestors = null;
    }
  });
}