slotted_render_object_widget.dart 11.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';

import 'framework.dart';

/// A mixin for a [RenderObjectWidget] that configures a [RenderObject]
/// subclass, which organizes its children in different slots.
///
/// Implementers of this mixin have to provide the list of available slots by
/// overriding [slots]. The list of slots must never change for a given class
/// implementing this mixin. In the common case, [Enum] values are used as slots
/// and [slots] is typically implemented to return the value of the enum's
/// `values` getter.
///
/// Furthermore, [childForSlot] must be implemented to return the current
/// widget configuration for a given slot.
///
/// The [RenderObject] returned by [createRenderObject] and updated by
/// [updateRenderObject] must implement the [SlottedContainerRenderObjectMixin].
///
/// The type parameter `S` is the type for the slots to be used by this
/// [RenderObjectWidget] and the [RenderObject] it configures. In the typical
/// case, `S` is an [Enum] type.
///
/// {@tool dartpad}
/// This example uses the [SlottedMultiChildRenderObjectWidgetMixin] in
/// combination with the [SlottedContainerRenderObjectMixin] to implement a
/// widget that provides two slots: topLeft and bottomRight. The widget arranges
/// the children in those slots diagonally.
///
/// ** See code in examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart **
/// {@end-tool}
///
/// See also:
///
///   * [MultiChildRenderObjectWidget], which configures a [RenderObject]
///     with a single list of children.
///   * [ListTile], which uses [SlottedMultiChildRenderObjectWidgetMixin] in its
///     internal (private) implementation.
mixin SlottedMultiChildRenderObjectWidgetMixin<S> on RenderObjectWidget {
  /// Returns a list of all available slots.
  ///
  /// The list of slots must be static and must never change for a given class
  /// implementing this mixin.
  ///
  /// Typically, an [Enum] is used to identify the different slots. In that case
  /// this getter can be implemented by returning what the `values` getter
  /// of the enum used returns.
  @protected
  Iterable<S> get slots;

  /// Returns the widget that is currently occupying the provided `slot`.
  ///
  /// The [RenderObject] configured by this class will be configured to have
  /// the [RenderObject] produced by the returned [Widget] in the provided
  /// `slot`.
  @protected
  Widget? childForSlot(S slot);

  @override
  SlottedContainerRenderObjectMixin<S> createRenderObject(BuildContext context);

  @override
  void updateRenderObject(BuildContext context, SlottedContainerRenderObjectMixin<S> renderObject);

  @override
  SlottedRenderObjectElement<S> createElement() => SlottedRenderObjectElement<S>(this);
}

/// Mixin for a [RenderBox] configured by a [SlottedMultiChildRenderObjectWidgetMixin].
///
/// The [RenderBox] child currently occupying a given slot can be obtained by
/// calling [childForSlot].
///
/// Implementers may consider overriding [children] to return the children
/// of this render object in a consistent order (e.g. hit test order).
///
/// The type parameter `S` is the type for the slots to be used by this
/// [RenderObject] and the [SlottedMultiChildRenderObjectWidgetMixin] it was
/// configured by. In the typical case, `S` is an [Enum] type.
///
/// See [SlottedMultiChildRenderObjectWidgetMixin] for example code showcasing
/// how this mixin is used in combination with the
/// [SlottedMultiChildRenderObjectWidgetMixin].
///
/// See also:
///
///  * [ContainerRenderObjectMixin], which organizes its children in a single
///    list.
mixin SlottedContainerRenderObjectMixin<S> on RenderBox {
  /// Returns the [RenderBox] child that is currently occupying the provided
  /// `slot`.
  ///
  /// Returns null if no [RenderBox] is configured for the given slot.
  @protected
  RenderBox? childForSlot(S slot) => _slotToChild[slot];

  /// Returns an [Iterable] of all non-null children.
  ///
  /// This getter is used by the default implementation of [attach], [detach],
  /// [redepthChildren], [visitChildren], and [debugDescribeChildren] to iterate
  /// over the children of this [RenderBox]. The base implementation makes no
  /// guarantee about the order in which the children are returned. Subclasses,
  /// for which the child order is important should override this getter and
  /// return the children in the desired order.
  @protected
  Iterable<RenderBox> get children => _slotToChild.values;

  /// Returns the debug name for a given `slot`.
  ///
  /// This method is called by [debugDescribeChildren] for each slot that is
  /// currently occupied by a child to obtain a name for that slot for debug
  /// outputs.
  ///
119
  /// The default implementation calls [EnumName.name] on `slot` if it is an
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
  /// [Enum] value and `toString` if it is not.
  @protected
  String debugNameForSlot(S slot) {
    if (slot is Enum) {
      return slot.name;
    }
    return slot.toString();
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    for (final RenderBox child in children) {
      child.attach(owner);
    }
  }

  @override
  void detach() {
    super.detach();
    for (final RenderBox child in children) {
      child.detach();
    }
  }

  @override
  void redepthChildren() {
    children.forEach(redepthChild);
  }

  @override
  void visitChildren(RenderObjectVisitor visitor) {
    children.forEach(visitor);
  }

  @override
  List<DiagnosticsNode> debugDescribeChildren() {
    final List<DiagnosticsNode> value = <DiagnosticsNode>[];
    final Map<RenderBox, S> childToSlot = Map<RenderBox, S>.fromIterables(
      _slotToChild.values,
      _slotToChild.keys,
    );
    for (final RenderBox child in children) {
      _addDiagnostics(child, value, debugNameForSlot(childToSlot[child] as S));
    }
    return value;
  }

  void _addDiagnostics(RenderBox child, List<DiagnosticsNode> value, String name) {
    value.add(child.toDiagnosticsNode(name: name));
  }

  final Map<S, RenderBox> _slotToChild = <S, RenderBox>{};

  void _setChild(RenderBox? child, S slot) {
    final RenderBox? oldChild = _slotToChild[slot];
    if (oldChild != null) {
      dropChild(oldChild);
      _slotToChild.remove(slot);
    }
    if (child != null) {
      _slotToChild[slot] = child;
      adoptChild(child);
    }
  }
185 186 187 188 189 190 191 192 193

  void _moveChild(RenderBox child, S slot, S oldSlot) {
    assert(slot != oldSlot);
    final RenderBox? oldChild = _slotToChild[oldSlot];
    if (oldChild == child) {
      _setChild(null, oldSlot);
    }
    _setChild(child, slot);
  }
194 195 196 197 198
}

/// Element used by the [SlottedMultiChildRenderObjectWidgetMixin].
class SlottedRenderObjectElement<S> extends RenderObjectElement {
  /// Creates an element that uses the given widget as its configuration.
199
  SlottedRenderObjectElement(SlottedMultiChildRenderObjectWidgetMixin<S> super.widget);
200

201 202
  Map<S, Element> _slotToChild = <S, Element>{};
  Map<Key, Element> _keyedChildren = <Key, Element>{};
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236

  @override
  SlottedContainerRenderObjectMixin<S> get renderObject => super.renderObject as SlottedContainerRenderObjectMixin<S>;

  @override
  void visitChildren(ElementVisitor visitor) {
    _slotToChild.values.forEach(visitor);
  }

  @override
  void forgetChild(Element child) {
    assert(_slotToChild.containsValue(child));
    assert(child.slot is S);
    assert(_slotToChild.containsKey(child.slot));
    _slotToChild.remove(child.slot);
    super.forgetChild(child);
  }

  @override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    _updateChildren();
  }

  @override
  void update(SlottedMultiChildRenderObjectWidgetMixin<S> newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _updateChildren();
  }

  List<S>? _debugPreviousSlots;

  void _updateChildren() {
237
    final SlottedMultiChildRenderObjectWidgetMixin<S> slottedMultiChildRenderObjectWidgetMixin = widget as SlottedMultiChildRenderObjectWidgetMixin<S>;
238
    assert(() {
239 240
      _debugPreviousSlots ??= slottedMultiChildRenderObjectWidgetMixin.slots.toList();
      return listEquals(_debugPreviousSlots, slottedMultiChildRenderObjectWidgetMixin.slots.toList());
241
    }(), '${widget.runtimeType}.slots must not change.');
242
    assert(slottedMultiChildRenderObjectWidgetMixin.slots.toSet().length == slottedMultiChildRenderObjectWidgetMixin.slots.length, 'slots must be unique');
243

244 245 246 247 248 249 250
    final Map<Key, Element> oldKeyedElements = _keyedChildren;
    _keyedChildren = <Key, Element>{};
    final Map<S, Element> oldSlotToChild = _slotToChild;
    _slotToChild = <S, Element>{};

    Map<Key, List<Element>>? debugDuplicateKeys;

251
    for (final S slot in slottedMultiChildRenderObjectWidgetMixin.slots) {
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
      final Widget? widget = slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot);
      final Key? newWidgetKey = widget?.key;

      final Element? oldSlotChild = oldSlotToChild[slot];
      final Element? oldKeyChild = oldKeyedElements[newWidgetKey];

      // Try to find the slot for the correct Element that `widget` should update.
      // If key matching fails, resort to `oldSlotChild` from the same slot.
      final Element? fromElement;
      if (oldKeyChild != null) {
        fromElement = oldSlotToChild.remove(oldKeyChild.slot as S);
      } else if (oldSlotChild?.widget.key == null) {
        fromElement = oldSlotToChild.remove(slot);
      } else {
        // The only case we can't use `oldSlotChild` is when its widget has a key.
        assert(oldSlotChild!.widget.key != newWidgetKey);
        fromElement = null;
      }
      final Element? newChild = updateChild(fromElement, widget, slot);

      if (newChild != null) {
        _slotToChild[slot] = newChild;

        if (newWidgetKey != null) {
          assert(() {
            final Element? existingElement = _keyedChildren[newWidgetKey];
            if (existingElement != null) {
              (debugDuplicateKeys ??= <Key, List<Element>>{})
                .putIfAbsent(newWidgetKey, () => <Element>[existingElement])
                .add(newChild);
            }
            return true;
          }());
          _keyedChildren[newWidgetKey] = newChild;
        }
      }
288
    }
289 290 291
    oldSlotToChild.values.forEach(deactivateChild);
    assert(_debugDuplicateKeys(debugDuplicateKeys));
    assert(_keyedChildren.values.every(_slotToChild.values.contains), '_keyedChildren ${_keyedChildren.values} should be a subset of ${_slotToChild.values}');
292 293
  }

294 295 296
  bool _debugDuplicateKeys(Map<Key, List<Element>>? debugDuplicateKeys) {
    if (debugDuplicateKeys == null) {
      return true;
297
    }
298 299 300 301
    for (final MapEntry<Key, List<Element>> duplicateKey in debugDuplicateKeys.entries) {
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('Multiple widgets used the same key in ${widget.runtimeType}.'),
        ErrorDescription(
302
          'The key ${duplicateKey.key} was used by multiple widgets. The offending widgets were:\n'
303 304 305 306 307 308
        ),
        for (final Element element in duplicateKey.value) ErrorDescription('  - $element\n'),
        ErrorDescription(
          'A key can only be specified on one widget at a time in the same parent widget.',
        ),
      ]);
309
    }
310
    return true;
311 312 313 314 315 316 317 318 319 320
  }

  @override
  void insertRenderObjectChild(RenderBox child, S slot) {
    renderObject._setChild(child, slot);
    assert(renderObject._slotToChild[slot] == child);
  }

  @override
  void removeRenderObjectChild(RenderBox child, S slot) {
321 322 323 324
    if (renderObject._slotToChild[slot] == child) {
      renderObject._setChild(null, slot);
      assert(renderObject._slotToChild[slot] == null);
    }
325 326 327
  }

  @override
328 329
  void moveRenderObjectChild(RenderBox child, S oldSlot, S newSlot) {
    renderObject._moveChild(child, newSlot, oldSlot);
330 331
  }
}