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 | |
5 | import 'package:flutter/foundation.dart'; |
6 | import 'package:flutter/rendering.dart'; |
7 | |
8 | import '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. |
49 | abstract 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 | ) |
65 | mixin 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. |
119 | mixin 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]. |
222 | 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 | @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 | |