1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6import 'package:flutter/rendering.dart';
7
8import 'framework.dart';
9
10/// A superclass for [RenderObjectWidget]s that configure [RenderObject]
11/// subclasses that organize their children in different slots.
12///
13/// Implementers of this mixin have to provide the list of available slots by
14/// overriding [slots]. The list of slots must never change for a given class
15/// implementing this mixin. In the common case, [Enum] values are used as slots
16/// and [slots] is typically implemented to return the value of the enum's
17/// `values` getter.
18///
19/// Furthermore, [childForSlot] must be implemented to return the current
20/// widget configuration for a given slot.
21///
22/// 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/// case, `SlotType` is an [Enum] type.
28///
29/// The type parameter `ChildType` is the type used for the [RenderObject] children
30/// (e.g. [RenderBox] or [RenderSliver]). In the typical case, `ChildType` is
31/// [RenderBox]. This class does not support having different kinds of children
32/// for different slots.
33///
34/// {@tool dartpad}
35/// This example uses the [SlottedMultiChildRenderObjectWidget] in
36/// combination with the [SlottedContainerRenderObjectMixin] to implement a
37/// widget that provides two slots: topLeft and bottomRight. The widget arranges
38/// the children in those slots diagonally.
39///
40/// ** See code in examples/api/lib/widgets/slotted_render_object_widget/slotted_multi_child_render_object_widget_mixin.0.dart **
41/// {@end-tool}
42///
43/// See also:
44///
45/// * [MultiChildRenderObjectWidget], which configures a [RenderObject]
46/// with a single list of children.
47/// * [ListTile], which uses [SlottedMultiChildRenderObjectWidget] in its
48/// internal (private) implementation.
49abstract class SlottedMultiChildRenderObjectWidget<SlotType, ChildType extends RenderObject> extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> {
50 /// Abstract const constructor. This constructor enables subclasses to provide
51 /// const constructors so that they can be used in const expressions.
52 const SlottedMultiChildRenderObjectWidget({ super.key });
53}
54
55/// A mixin version of [SlottedMultiChildRenderObjectWidget].
56///
57/// This mixin provides the same logic as extending
58/// [SlottedMultiChildRenderObjectWidget] directly.
59///
60/// It was deprecated to simplify the process of creating slotted widgets.
61@Deprecated(
62 'Extend SlottedMultiChildRenderObjectWidget instead of mixing in SlottedMultiChildRenderObjectWidgetMixin. '
63 'This feature was deprecated after v3.10.0-1.5.pre.'
64)
65mixin SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType extends RenderObject> on RenderObjectWidget {
66 /// Returns a list of all available slots.
67 ///
68 /// The list of slots must be static and must never change for a given class
69 /// implementing this mixin.
70 ///
71 /// Typically, an [Enum] is used to identify the different slots. In that case
72 /// this getter can be implemented by returning what the `values` getter
73 /// of the enum used returns.
74 @protected
75 Iterable<SlotType> get slots;
76
77 /// Returns the widget that is currently occupying the provided `slot`.
78 ///
79 /// The [RenderObject] configured by this class will be configured to have
80 /// the [RenderObject] produced by the returned [Widget] in the provided
81 /// `slot`.
82 @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/// calling [childForSlot].
99///
100/// Implementers may consider overriding [children] to return the children
101/// of this render object in a consistent order (e.g. hit test order).
102///
103/// The type parameter `SlotType` is the type for the slots to be used by this
104/// [RenderObject] and the [SlottedMultiChildRenderObjectWidget] it was
105/// configured by. In the typical case, `SlotType` is an [Enum] type.
106///
107/// The type parameter `ChildType` is the type of [RenderObject] used for the children
108/// (e.g. [RenderBox] or [RenderSliver]). In the typical case, `ChildType` is
109/// [RenderBox]. This mixin does not support having different kinds of children
110/// for different slots.
111///
112/// See [SlottedMultiChildRenderObjectWidget] for example code showcasing how
113/// this mixin is used in combination with [SlottedMultiChildRenderObjectWidget].
114///
115/// See also:
116///
117/// * [ContainerRenderObjectMixin], which organizes its children in a single
118/// list.
119mixin SlottedContainerRenderObjectMixin<SlotType, ChildType extends RenderObject> on RenderObject {
120 /// Returns the [RenderObject] child that is currently occupying the provided
121 /// `slot`.
122 ///
123 /// Returns null if no [RenderObject] is configured for the given slot.
124 @protected
125 ChildType? childForSlot(SlotType slot) => _slotToChild[slot];
126
127 /// Returns an [Iterable] of all non-null children.
128 ///
129 /// This getter is used by the default implementation of [attach], [detach],
130 /// [redepthChildren], [visitChildren], and [debugDescribeChildren] to iterate
131 /// over the children of this [RenderObject]. The base implementation makes no
132 /// guarantee about the order in which the children are returned. Subclasses
133 /// for which the child order is important should override this getter and
134 /// return the children in the desired order.
135 @protected
136 Iterable<ChildType> get children => _slotToChild.values;
137
138 /// Returns the debug name for a given `slot`.
139 ///
140 /// This method is called by [debugDescribeChildren] for each slot that is
141 /// currently occupied by a child to obtain a name for that slot for debug
142 /// outputs.
143 ///
144 /// The default implementation calls [EnumName.name] on `slot` if it is an
145 /// [Enum] value and `toString` if it is not.
146 @protected
147 String debugNameForSlot(SlotType slot) {
148 if (slot is Enum) {
149 return slot.name;
150 }
151 return slot.toString();
152 }
153
154 @override
155 void attach(PipelineOwner owner) {
156 super.attach(owner);
157 for (final ChildType child in children) {
158 child.attach(owner);
159 }
160 }
161
162 @override
163 void detach() {
164 super.detach();
165 for (final ChildType child in children) {
166 child.detach();
167 }
168 }
169
170 @override
171 void redepthChildren() {
172 children.forEach(redepthChild);
173 }
174
175 @override
176 void visitChildren(RenderObjectVisitor visitor) {
177 children.forEach(visitor);
178 }
179
180 @override
181 List<DiagnosticsNode> debugDescribeChildren() {
182 final List<DiagnosticsNode> value = <DiagnosticsNode>[];
183 final Map<ChildType, SlotType> childToSlot = Map<ChildType, SlotType>.fromIterables(
184 _slotToChild.values,
185 _slotToChild.keys,
186 );
187 for (final ChildType child in children) {
188 _addDiagnostics(child, value, debugNameForSlot(childToSlot[child] as SlotType));
189 }
190 return value;
191 }
192
193 void _addDiagnostics(ChildType child, List<DiagnosticsNode> value, String name) {
194 value.add(child.toDiagnosticsNode(name: name));
195 }
196
197 final Map<SlotType, ChildType> _slotToChild = <SlotType, ChildType>{};
198
199 void _setChild(ChildType? child, SlotType slot) {
200 final ChildType? oldChild = _slotToChild[slot];
201 if (oldChild != null) {
202 dropChild(oldChild);
203 _slotToChild.remove(slot);
204 }
205 if (child != null) {
206 _slotToChild[slot] = child;
207 adoptChild(child);
208 }
209 }
210
211 void _moveChild(ChildType child, SlotType slot, SlotType oldSlot) {
212 assert(slot != oldSlot);
213 final ChildType? oldChild = _slotToChild[oldSlot];
214 if (oldChild == child) {
215 _setChild(null, oldSlot);
216 }
217 _setChild(child, slot);
218 }
219}
220
221/// Element used by the [SlottedMultiChildRenderObjectWidget].
222class 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 @override
233 void visitChildren(ElementVisitor visitor) {
234 _slotToChild.values.forEach(visitor);
235 }
236
237 @override
238 void forgetChild(Element child) {
239 assert(_slotToChild.containsValue(child));
240 assert(child.slot is SlotType);
241 assert(_slotToChild.containsKey(child.slot));
242 _slotToChild.remove(child.slot);
243 super.forgetChild(child);
244 }
245
246 @override
247 void mount(Element? parent, Object? newSlot) {
248 super.mount(parent, newSlot);
249 _updateChildren();
250 }
251
252 @override
253 void update(SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> newWidget) {
254 super.update(newWidget);
255 assert(widget == newWidget);
256 _updateChildren();
257 }
258
259 List<SlotType>? _debugPreviousSlots;
260
261 void _updateChildren() {
262 final SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType> slottedMultiChildRenderObjectWidgetMixin = widget as SlottedMultiChildRenderObjectWidgetMixin<SlotType, ChildType>;
263 assert(() {
264 _debugPreviousSlots ??= slottedMultiChildRenderObjectWidgetMixin.slots.toList();
265 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 final Map<Key, Element> oldKeyedElements = _keyedChildren;
270 _keyedChildren = <Key, Element>{};
271 final Map<SlotType, Element> oldSlotToChild = _slotToChild;
272 _slotToChild = <SlotType, Element>{};
273
274 Map<Key, List<Element>>? debugDuplicateKeys;
275
276 for (final SlotType slot in slottedMultiChildRenderObjectWidgetMixin.slots) {
277 final Widget? widget = slottedMultiChildRenderObjectWidgetMixin.childForSlot(slot);
278 final Key? newWidgetKey = widget?.key;
279
280 final Element? oldSlotChild = oldSlotToChild[slot];
281 final Element? oldKeyChild = oldKeyedElements[newWidgetKey];
282
283 // Try to find the slot for the correct Element that `widget` should update.
284 // If key matching fails, resort to `oldSlotChild` from the same slot.
285 final Element? fromElement;
286 if (oldKeyChild != null) {
287 fromElement = oldSlotToChild.remove(oldKeyChild.slot as SlotType);
288 } else if (oldSlotChild?.widget.key == null) {
289 fromElement = oldSlotToChild.remove(slot);
290 } else {
291 // The only case we can't use `oldSlotChild` is when its widget has a key.
292 assert(oldSlotChild!.widget.key != newWidgetKey);
293 fromElement = null;
294 }
295 final Element? newChild = updateChild(fromElement, widget, slot);
296
297 if (newChild != null) {
298 _slotToChild[slot] = newChild;
299
300 if (newWidgetKey != null) {
301 assert(() {
302 final Element? existingElement = _keyedChildren[newWidgetKey];
303 if (existingElement != null) {
304 (debugDuplicateKeys ??= <Key, List<Element>>{})
305 .putIfAbsent(newWidgetKey, () => <Element>[existingElement])
306 .add(newChild);
307 }
308 return true;
309 }());
310 _keyedChildren[newWidgetKey] = newChild;
311 }
312 }
313 }
314 oldSlotToChild.values.forEach(deactivateChild);
315 assert(_debugDuplicateKeys(debugDuplicateKeys));
316 assert(_keyedChildren.values.every(_slotToChild.values.contains), '_keyedChildren ${_keyedChildren.values} should be a subset of ${_slotToChild.values}');
317 }
318
319 bool _debugDuplicateKeys(Map<Key, List<Element>>? debugDuplicateKeys) {
320 if (debugDuplicateKeys == null) {
321 return true;
322 }
323 for (final MapEntry<Key, List<Element>> duplicateKey in debugDuplicateKeys.entries) {
324 throw FlutterError.fromParts(<DiagnosticsNode>[
325 ErrorSummary('Multiple widgets used the same key in ${widget.runtimeType}.'),
326 ErrorDescription(
327 'The key ${duplicateKey.key} was used by multiple widgets. The offending widgets were:\n'
328 ),
329 for (final Element element in duplicateKey.value) ErrorDescription(' - $element\n'),
330 ErrorDescription(
331 'A key can only be specified on one widget at a time in the same parent widget.',
332 ),
333 ]);
334 }
335 return true;
336 }
337
338 @override
339 void insertRenderObjectChild(ChildType child, SlotType slot) {
340 renderObject._setChild(child, slot);
341 assert(renderObject._slotToChild[slot] == child);
342 }
343
344 @override
345 void removeRenderObjectChild(ChildType child, SlotType slot) {
346 if (renderObject._slotToChild[slot] == child) {
347 renderObject._setChild(null, slot);
348 assert(renderObject._slotToChild[slot] == null);
349 }
350 }
351
352 @override
353 void moveRenderObjectChild(ChildType child, SlotType oldSlot, SlotType newSlot) {
354 renderObject._moveChild(child, newSlot, oldSlot);
355 }
356}
357