slotted_render_object_widget.dart 13.5 KB
Newer Older
1 2 3 4 5 6 7 8 9
// 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';

10 11
/// A superclass for [RenderObjectWidget]s that configure [RenderObject]
/// subclasses that organize their children in different slots.
12 13 14 15 16 17 18 19 20 21 22
///
/// 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
23
/// [updateRenderObject] must implement [SlottedContainerRenderObjectMixin].
24
///
25
/// The type parameter `SlotType` is the type for the slots to be used by this
26
/// [RenderObjectWidget] and the [RenderObject] it configures. In the typical
27 28 29 30 31 32
/// case, `SlotType` is an [Enum] type.
///
/// The type parameter `ChildType` is the type used for the [RenderObject] children
/// (e.g. [RenderBox] or [RenderSliver]). In the typical case, `ChildType` is
/// [RenderBox]. This class does not support having different kinds of children
/// for different slots.
33 34
///
/// {@tool dartpad}
35
/// This example uses the [SlottedMultiChildRenderObjectWidget] in
36 37 38 39 40 41 42 43 44 45 46
/// 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.
47
///   * [ListTile], which uses [SlottedMultiChildRenderObjectWidget] in its
48
///     internal (private) implementation.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
abstract class SlottedMultiChildRenderObjectWidget<SlotType, ChildType extends RenderObject> extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const SlottedMultiChildRenderObjectWidget({ super.key });
}

/// A mixin version of [SlottedMultiChildRenderObjectWidget].
///
/// This mixin provides the same logic as extending
/// [SlottedMultiChildRenderObjectWidget] directly.
///
/// It was deprecated to simplify the process of creating slotted widgets.
@Deprecated(
  'Extend SlottedMultiChildRenderObjectWidget instead of mixing in SlottedMultiChildRenderObjectWidgetMixin. '
  'This feature was deprecated after v3.10.0-1.5.pre.'
)
mixin SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType extends RenderObject> on RenderObjectWidget {
66 67 68 69 70 71 72 73 74
  /// 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
75
  Iterable<SlotType> get slots;
76 77 78 79 80 81 82

  /// 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
83
  Widget? childForSlot(SlotType slot);
84 85

  @override
86
  SlottedContainerRenderObjectMixin<SlotType, ChildType> createRenderObject(BuildContext context);
87 88

  @override
89
  void updateRenderObject(BuildContext context, SlottedContainerRenderObjectMixin<SlotType, ChildType> renderObject);
90 91

  @override
92
  SlottedRenderObjectElement<SlotType, ChildType> createElement() => SlottedRenderObjectElement<SlotType, ChildType>(this);
93 94
}

95
/// Mixin for a [RenderObject] configured by a [SlottedMultiChildRenderObjectWidget].
96
///
97
/// The [RenderObject] child currently occupying a given slot can be obtained by
98 99 100 101 102
/// calling [childForSlot].
///
/// Implementers may consider overriding [children] to return the children
/// of this render object in a consistent order (e.g. hit test order).
///
103 104 105 106 107 108 109 110
/// The type parameter `SlotType` is the type for the slots to be used by this
/// [RenderObject] and the [SlottedMultiChildRenderObjectWidget] it was
/// configured by. In the typical case, `SlotType` is an [Enum] type.
///
/// The type parameter `ChildType` is the type of [RenderObject] used for the children
/// (e.g. [RenderBox] or [RenderSliver]). In the typical case, `ChildType` is
/// [RenderBox]. This mixin does not support having different kinds of children
/// for different slots.
111
///
112 113
/// See [SlottedMultiChildRenderObjectWidget] for example code showcasing how
/// this mixin is used in combination with [SlottedMultiChildRenderObjectWidget].
114 115 116 117 118
///
/// See also:
///
///  * [ContainerRenderObjectMixin], which organizes its children in a single
///    list.
119 120
mixin SlottedContainerRenderObjectMixin<SlotType, ChildType extends RenderObject> on RenderObject {
  /// Returns the [RenderObject] child that is currently occupying the provided
121 122
  /// `slot`.
  ///
123
  /// Returns null if no [RenderObject] is configured for the given slot.
124
  @protected
125
  ChildType? childForSlot(SlotType slot) => _slotToChild[slot];
126 127 128 129 130

  /// 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
131 132
  /// over the children of this [RenderObject]. The base implementation makes no
  /// guarantee about the order in which the children are returned. Subclasses
133 134 135
  /// for which the child order is important should override this getter and
  /// return the children in the desired order.
  @protected
136
  Iterable<ChildType> get children => _slotToChild.values;
137 138 139 140 141 142 143

  /// 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.
  ///
144
  /// The default implementation calls [EnumName.name] on `slot` if it is an
145 146
  /// [Enum] value and `toString` if it is not.
  @protected
147
  String debugNameForSlot(SlotType slot) {
148 149 150 151 152 153 154 155 156
    if (slot is Enum) {
      return slot.name;
    }
    return slot.toString();
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
157
    for (final ChildType child in children) {
158 159 160 161 162 163 164
      child.attach(owner);
    }
  }

  @override
  void detach() {
    super.detach();
165
    for (final ChildType child in children) {
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
      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>[];
183
    final Map<ChildType, SlotType> childToSlot = Map<ChildType, SlotType>.fromIterables(
184 185 186
      _slotToChild.values,
      _slotToChild.keys,
    );
187 188
    for (final ChildType child in children) {
      _addDiagnostics(child, value, debugNameForSlot(childToSlot[child] as SlotType));
189 190 191 192
    }
    return value;
  }

193
  void _addDiagnostics(ChildType child, List<DiagnosticsNode> value, String name) {
194 195 196
    value.add(child.toDiagnosticsNode(name: name));
  }

197
  final Map<SlotType, ChildType> _slotToChild = <SlotType, ChildType>{};
198

199 200
  void _setChild(ChildType? child, SlotType slot) {
    final ChildType? oldChild = _slotToChild[slot];
201 202 203 204 205 206 207 208 209
    if (oldChild != null) {
      dropChild(oldChild);
      _slotToChild.remove(slot);
    }
    if (child != null) {
      _slotToChild[slot] = child;
      adoptChild(child);
    }
  }
210

211
  void _moveChild(ChildType child, SlotType slot, SlotType oldSlot) {
212
    assert(slot != oldSlot);
213
    final ChildType? oldChild = _slotToChild[oldSlot];
214 215 216 217 218
    if (oldChild == child) {
      _setChild(null, oldSlot);
    }
    _setChild(child, slot);
  }
219 220
}

221 222
/// Element used by the [SlottedMultiChildRenderObjectWidget].
class SlottedRenderObjectElement<SlotType, ChildType extends RenderObject> extends RenderObjectElement {
223
  /// Creates an element that uses the given widget as its configuration.
224
  SlottedRenderObjectElement(SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> super.widget);
225

226
  Map<SlotType, Element> _slotToChild = <SlotType, Element>{};
227
  Map<Key, Element> _keyedChildren = <Key, Element>{};
228 229

  @override
230
  SlottedContainerRenderObjectMixin<SlotType, ChildType> get renderObject => super.renderObject as SlottedContainerRenderObjectMixin<SlotType, ChildType>;
231 232 233 234 235 236 237 238 239

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

  @override
  void forgetChild(Element child) {
    assert(_slotToChild.containsValue(child));
240
    assert(child.slot is SlotType);
241 242 243 244 245 246 247 248 249 250 251 252
    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
253
  void update(SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> newWidget) {
254 255 256 257 258
    super.update(newWidget);
    assert(widget == newWidget);
    _updateChildren();
  }

259
  List<SlotType>? _debugPreviousSlots;
260 261

  void _updateChildren() {
262
    final SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> slottedMultiChildRenderObjectWidgetMixin = widget as SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType>;
263
    assert(() {
264 265
      _debugPreviousSlots ??= slottedMultiChildRenderObjectWidgetMixin.slots.toList();
      return listEquals(_debugPreviousSlots, slottedMultiChildRenderObjectWidgetMixin.slots.toList());
266
    }(), '${widget.runtimeType}.slots must not change.');
267
    assert(slottedMultiChildRenderObjectWidgetMixin.slots.toSet().length == slottedMultiChildRenderObjectWidgetMixin.slots.length, 'slots must be unique');
268

269 270
    final Map<Key, Element> oldKeyedElements = _keyedChildren;
    _keyedChildren = <Key, Element>{};
271 272
    final Map<SlotType, Element> oldSlotToChild = _slotToChild;
    _slotToChild = <SlotType, Element>{};
273 274 275

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

276
    for (final SlotType slot in slottedMultiChildRenderObjectWidgetMixin.slots) {
277 278 279 280 281 282 283 284 285 286
      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) {
287
        fromElement = oldSlotToChild.remove(oldKeyChild.slot as SlotType);
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
      } 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;
        }
      }
313
    }
314 315 316
    oldSlotToChild.values.forEach(deactivateChild);
    assert(_debugDuplicateKeys(debugDuplicateKeys));
    assert(_keyedChildren.values.every(_slotToChild.values.contains), '_keyedChildren ${_keyedChildren.values} should be a subset of ${_slotToChild.values}');
317 318
  }

319 320 321
  bool _debugDuplicateKeys(Map<Key, List<Element>>? debugDuplicateKeys) {
    if (debugDuplicateKeys == null) {
      return true;
322
    }
323 324 325 326
    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(
327
          'The key ${duplicateKey.key} was used by multiple widgets. The offending widgets were:\n'
328 329 330 331 332 333
        ),
        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.',
        ),
      ]);
334
    }
335
    return true;
336 337 338
  }

  @override
339
  void insertRenderObjectChild(ChildType child, SlotType slot) {
340 341 342 343 344
    renderObject._setChild(child, slot);
    assert(renderObject._slotToChild[slot] == child);
  }

  @override
345
  void removeRenderObjectChild(ChildType child, SlotType slot) {
346 347 348 349
    if (renderObject._slotToChild[slot] == child) {
      renderObject._setChild(null, slot);
      assert(renderObject._slotToChild[slot] == null);
    }
350 351 352
  }

  @override
353
  void moveRenderObjectChild(ChildType child, SlotType oldSlot, SlotType newSlot) {
354
    renderObject._moveChild(child, newSlot, oldSlot);
355 356
  }
}