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 | /// @docImport 'basic.dart'; |
6 | /// @docImport 'single_child_scroll_view.dart'; |
7 | /// @docImport 'sliver_layout_builder.dart'; |
8 | library; |
9 | |
10 | import 'package:flutter/foundation.dart'; |
11 | import 'package:flutter/rendering.dart'; |
12 | import 'package:flutter/scheduler.dart'; |
13 | |
14 | import 'debug.dart'; |
15 | import 'framework.dart'; |
16 | |
17 | /// The signature of the [LayoutBuilder] builder function. |
18 | typedef LayoutWidgetBuilder = Widget Function(BuildContext context, BoxConstraints constraints); |
19 | |
20 | /// An abstract superclass for widgets that defer their building until layout. |
21 | /// |
22 | /// Similar to the [Builder] widget except that the implementation calls the [builder] |
23 | /// function at layout time and provides the [LayoutInfoType] that is required to |
24 | /// configure the child widget subtree. |
25 | /// |
26 | /// This is useful when the child widget tree relies on information that are only |
27 | /// available during layout, and doesn't depend on the child's intrinsic size. |
28 | /// |
29 | /// The [LayoutInfoType] should typically be immutable. The equality of the |
30 | /// [LayoutInfoType] type is used by the implementation to avoid unnecessary |
31 | /// rebuilds: if the new [LayoutInfoType] computed during layout is the same as |
32 | /// (defined by `LayoutInfoType.==`) the previous [LayoutInfoType], the |
33 | /// implementation will try to avoid calling the [builder] again unless |
34 | /// [updateShouldRebuild] returns true. The corresponding [RenderObject] produced |
35 | /// by this widget retains the most up-to-date [LayoutInfoType] for this purpose, |
36 | /// which may keep a [LayoutInfoType] object in memory until the widget is removed |
37 | /// from the tree. |
38 | /// |
39 | /// Subclasses must return a [RenderObject] that mixes in [RenderAbstractLayoutBuilderMixin]. |
40 | abstract class AbstractLayoutBuilder<LayoutInfoType> extends RenderObjectWidget { |
41 | /// Creates a widget that defers its building until layout. |
42 | const AbstractLayoutBuilder({super.key}); |
43 | |
44 | /// Called at layout time to construct the widget tree. |
45 | /// |
46 | /// The builder must not return null. |
47 | Widget Function(BuildContext context, LayoutInfoType layoutInfo) get builder; |
48 | |
49 | @override |
50 | RenderObjectElement createElement() => _LayoutBuilderElement<LayoutInfoType>(this); |
51 | |
52 | /// Whether [builder] needs to be called again even if the layout constraints |
53 | /// are the same. |
54 | /// |
55 | /// When this widget's configuration is updated, the [builder] callback most |
56 | /// likely needs to be called to build this widget's child. However, |
57 | /// subclasses may provide ways in which the widget can be updated without |
58 | /// needing to rebuild the child. Such subclasses can use this method to tell |
59 | /// the framework when the child widget should be rebuilt. |
60 | /// |
61 | /// When this method is called by the framework, the newly configured widget |
62 | /// is asked if it requires a rebuild, and it is passed the old widget as a |
63 | /// parameter. |
64 | /// |
65 | /// See also: |
66 | /// |
67 | /// * [State.setState] and [State.didUpdateWidget], which talk about widget |
68 | /// configuration changes and how they're triggered. |
69 | /// * [Element.update], the method that actually updates the widget's |
70 | /// configuration. |
71 | @protected |
72 | bool updateShouldRebuild(covariant AbstractLayoutBuilder<LayoutInfoType> oldWidget) => true; |
73 | |
74 | @override |
75 | RenderAbstractLayoutBuilderMixin<LayoutInfoType, RenderObject> createRenderObject( |
76 | BuildContext context, |
77 | ); |
78 | |
79 | // updateRenderObject is redundant with the logic in the LayoutBuilderElement below. |
80 | } |
81 | |
82 | /// A specialized [AbstractLayoutBuilder] whose widget subtree depends on the |
83 | /// incoming [ConstraintType] that will be imposed on the widget. |
84 | /// |
85 | /// {@template flutter.widgets.ConstrainedLayoutBuilder} |
86 | /// The [builder] function is called in the following situations: |
87 | /// |
88 | /// * The first time the widget is laid out. |
89 | /// * When the parent widget passes different layout constraints. |
90 | /// * When the parent widget updates this widget and [updateShouldRebuild] returns `true`. |
91 | /// * When the dependencies that the [builder] function subscribes to change. |
92 | /// |
93 | /// The [builder] function is _not_ called during layout if the parent passes |
94 | /// the same constraints repeatedly. |
95 | /// |
96 | /// In the event that an ancestor skips the layout of this subtree so the |
97 | /// constraints become outdated, the `builder` rebuilds with the last known |
98 | /// constraints. |
99 | /// {@endtemplate} |
100 | abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints> |
101 | extends AbstractLayoutBuilder<ConstraintType> { |
102 | /// Creates a widget that defers its building until layout. |
103 | const ConstrainedLayoutBuilder({super.key, required this.builder}); |
104 | |
105 | @override |
106 | final Widget Function(BuildContext context, ConstraintType constraints) builder; |
107 | } |
108 | |
109 | class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement { |
110 | _LayoutBuilderElement(AbstractLayoutBuilder<LayoutInfoType> super.widget); |
111 | |
112 | @override |
113 | RenderAbstractLayoutBuilderMixin<LayoutInfoType, RenderObject> get renderObject => |
114 | super.renderObject as RenderAbstractLayoutBuilderMixin<LayoutInfoType, RenderObject>; |
115 | |
116 | Element? _child; |
117 | |
118 | @override |
119 | BuildScope get buildScope => _buildScope; |
120 | |
121 | late final BuildScope _buildScope = BuildScope(scheduleRebuild: _scheduleRebuild); |
122 | |
123 | // To schedule a rebuild, markNeedsLayout needs to be called on this Element's |
124 | // render object (as the rebuilding is done in its performLayout call). However, |
125 | // the render tree should typically be kept clean during the postFrameCallbacks |
126 | // and the idle phase, so the layout data can be safely read. |
127 | bool _deferredCallbackScheduled = false; |
128 | void _scheduleRebuild() { |
129 | if (_deferredCallbackScheduled) { |
130 | return; |
131 | } |
132 | |
133 | final bool deferMarkNeedsLayout = switch (SchedulerBinding.instance.schedulerPhase) { |
134 | SchedulerPhase.idle || SchedulerPhase.postFrameCallbacks => true, |
135 | SchedulerPhase.transientCallbacks || |
136 | SchedulerPhase.midFrameMicrotasks || |
137 | SchedulerPhase.persistentCallbacks => false, |
138 | }; |
139 | if (!deferMarkNeedsLayout) { |
140 | renderObject.scheduleLayoutCallback(); |
141 | return; |
142 | } |
143 | _deferredCallbackScheduled = true; |
144 | SchedulerBinding.instance.scheduleFrameCallback(_frameCallback); |
145 | } |
146 | |
147 | void _frameCallback(Duration timestamp) { |
148 | _deferredCallbackScheduled = false; |
149 | // This method is only called when the render tree is stable, if the Element |
150 | // is deactivated it will never be reincorporated back to the tree. |
151 | if (mounted) { |
152 | renderObject.scheduleLayoutCallback(); |
153 | } |
154 | } |
155 | |
156 | @override |
157 | void visitChildren(ElementVisitor visitor) { |
158 | if (_child != null) { |
159 | visitor(_child!); |
160 | } |
161 | } |
162 | |
163 | @override |
164 | void forgetChild(Element child) { |
165 | assert(child == _child); |
166 | _child = null; |
167 | super.forgetChild(child); |
168 | } |
169 | |
170 | @override |
171 | void mount(Element? parent, Object? newSlot) { |
172 | super.mount(parent, newSlot); // Creates the renderObject. |
173 | renderObject._updateCallback(_rebuildWithConstraints); |
174 | } |
175 | |
176 | @override |
177 | void update(AbstractLayoutBuilder<LayoutInfoType> newWidget) { |
178 | assert(widget != newWidget); |
179 | final AbstractLayoutBuilder<LayoutInfoType> oldWidget = |
180 | widget as AbstractLayoutBuilder<LayoutInfoType>; |
181 | super.update(newWidget); |
182 | assert(widget == newWidget); |
183 | |
184 | renderObject._updateCallback(_rebuildWithConstraints); |
185 | if (newWidget.updateShouldRebuild(oldWidget)) { |
186 | _needsBuild = true; |
187 | renderObject.scheduleLayoutCallback(); |
188 | } |
189 | } |
190 | |
191 | @override |
192 | void markNeedsBuild() { |
193 | // Calling super.markNeedsBuild is not needed. This Element does not need |
194 | // to performRebuild since this call already does what performRebuild does, |
195 | // So the element is clean as soon as this method returns and does not have |
196 | // to be added to the dirty list or marked as dirty. |
197 | renderObject.scheduleLayoutCallback(); |
198 | _needsBuild = true; |
199 | } |
200 | |
201 | @override |
202 | void performRebuild() { |
203 | // This gets called if markNeedsBuild() is called on us. |
204 | // That might happen if, e.g., our builder uses Inherited widgets. |
205 | |
206 | // Force the callback to be called, even if the layout constraints are the |
207 | // same. This is because that callback may depend on the updated widget |
208 | // configuration, or an inherited widget. |
209 | renderObject.scheduleLayoutCallback(); |
210 | _needsBuild = true; |
211 | super.performRebuild(); // Calls widget.updateRenderObject (a no-op in this case). |
212 | } |
213 | |
214 | @override |
215 | void unmount() { |
216 | renderObject._callback = null; |
217 | super.unmount(); |
218 | } |
219 | |
220 | // The LayoutInfoType that was used to invoke the layout callback with last time, |
221 | // during layout. The `_previousLayoutInfo` value is compared to the new one |
222 | // to determine whether [LayoutBuilderBase.builder] needs to be called. |
223 | LayoutInfoType? _previousLayoutInfo; |
224 | bool _needsBuild = true; |
225 | |
226 | void _rebuildWithConstraints(Constraints _) { |
227 | final LayoutInfoType layoutInfo = renderObject.layoutInfo; |
228 | @pragma('vm:notify-debugger-on-exception') |
229 | void updateChildCallback() { |
230 | Widget built; |
231 | try { |
232 | assert(layoutInfo == renderObject.layoutInfo); |
233 | built = (widget as AbstractLayoutBuilder<LayoutInfoType>).builder(this, layoutInfo); |
234 | debugWidgetBuilderValue(widget, built); |
235 | } catch (e, stack) { |
236 | built = ErrorWidget.builder( |
237 | _reportException( |
238 | ErrorDescription('building$widget '), |
239 | e, |
240 | stack, |
241 | informationCollector: |
242 | () => <DiagnosticsNode>[ |
243 | if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)), |
244 | ], |
245 | ), |
246 | ); |
247 | } |
248 | try { |
249 | _child = updateChild(_child, built, null); |
250 | assert(_child != null); |
251 | } catch (e, stack) { |
252 | built = ErrorWidget.builder( |
253 | _reportException( |
254 | ErrorDescription('building$widget '), |
255 | e, |
256 | stack, |
257 | informationCollector: |
258 | () => <DiagnosticsNode>[ |
259 | if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)), |
260 | ], |
261 | ), |
262 | ); |
263 | _child = updateChild(null, built, slot); |
264 | } finally { |
265 | _needsBuild = false; |
266 | _previousLayoutInfo = layoutInfo; |
267 | } |
268 | } |
269 | |
270 | final VoidCallback? callback = |
271 | _needsBuild || (layoutInfo != _previousLayoutInfo) ? updateChildCallback : null; |
272 | owner!.buildScope(this, callback); |
273 | } |
274 | |
275 | @override |
276 | void insertRenderObjectChild(RenderObject child, Object? slot) { |
277 | final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject; |
278 | assert(slot == null); |
279 | assert(renderObject.debugValidateChild(child)); |
280 | renderObject.child = child; |
281 | assert(renderObject == this.renderObject); |
282 | } |
283 | |
284 | @override |
285 | void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) { |
286 | assert(false); |
287 | } |
288 | |
289 | @override |
290 | void removeRenderObjectChild(RenderObject child, Object? slot) { |
291 | final RenderAbstractLayoutBuilderMixin<LayoutInfoType, RenderObject> renderObject = |
292 | this.renderObject; |
293 | assert(renderObject.child == child); |
294 | renderObject.child = null; |
295 | assert(renderObject == this.renderObject); |
296 | } |
297 | } |
298 | |
299 | /// Generic mixin for [RenderObject]s created by an [AbstractLayoutBuilder] with |
300 | /// the the same `LayoutInfoType`. |
301 | /// |
302 | /// Provides a [layoutCallback] implementation which, if needed, invokes |
303 | /// [AbstractLayoutBuilder]'s builder callback. |
304 | /// |
305 | /// Implementers can override the [layoutInfo] implementation with a value |
306 | /// that is safe to access in [layoutCallback], which is called in |
307 | /// [performLayout]. The default [layoutInfo] returns the incoming |
308 | /// [Constraints]. |
309 | /// |
310 | /// This mixin replaces [RenderConstrainedLayoutBuilder]. |
311 | mixin RenderAbstractLayoutBuilderMixin<LayoutInfoType, ChildType extends RenderObject> |
312 | on RenderObjectWithChildMixin<ChildType>, RenderObjectWithLayoutCallbackMixin { |
313 | LayoutCallback<Constraints>? _callback; |
314 | |
315 | /// Change the layout callback. |
316 | void _updateCallback(LayoutCallback<Constraints> value) { |
317 | if (value == _callback) { |
318 | return; |
319 | } |
320 | _callback = value; |
321 | scheduleLayoutCallback(); |
322 | } |
323 | |
324 | /// Invokes the builder callback supplied via [AbstractLayoutBuilder] and |
325 | /// rebuilds the [AbstractLayoutBuilder]'s widget tree, if needed. |
326 | /// |
327 | /// No further work will be done if [layoutInfo] has not changed since the last |
328 | /// time this method was called, and [AbstractLayoutBuilder.updateShouldRebuild] |
329 | /// returned `false` when the widget was rebuilt. |
330 | /// |
331 | /// This method should typically be called as soon as possible in the class's |
332 | /// [performLayout] implementation, before any layout work is done. |
333 | @visibleForOverriding |
334 | @override |
335 | void layoutCallback() => _callback!(constraints); |
336 | |
337 | /// The information to invoke the [AbstractLayoutBuilder.builder] callback with. |
338 | /// |
339 | /// This is typically the information that are only made available in |
340 | /// [performLayout], which is inaccessible for regular [Builder] widget, |
341 | /// such as the incoming [Constraints], which are the default value. |
342 | @protected |
343 | LayoutInfoType get layoutInfo => constraints as LayoutInfoType; |
344 | } |
345 | |
346 | /// Generic mixin for [RenderObject]s created by an [AbstractLayoutBuilder] with |
347 | /// the the same `LayoutInfoType`. |
348 | /// |
349 | /// Use [RenderAbstractLayoutBuilderMixin] instead, which replaces this mixin. |
350 | typedef RenderConstrainedLayoutBuilder<LayoutInfoType, ChildType extends RenderObject> = |
351 | RenderAbstractLayoutBuilderMixin<LayoutInfoType, ChildType>; |
352 | |
353 | /// Builds a widget tree that can depend on the parent widget's size. |
354 | /// |
355 | /// Similar to the [Builder] widget except that the framework calls the [builder] |
356 | /// function at layout time and provides the parent widget's constraints. This |
357 | /// is useful when the parent constrains the child's size and doesn't depend on |
358 | /// the child's intrinsic size. The [LayoutBuilder]'s final size will match its |
359 | /// child's size. |
360 | /// |
361 | /// {@macro flutter.widgets.ConstrainedLayoutBuilder} |
362 | /// |
363 | /// {@youtube 560 315 https://www.youtube.com/watch?v=IYDVcriKjsw} |
364 | /// |
365 | /// If the child should be smaller than the parent, consider wrapping the child |
366 | /// in an [Align] widget. If the child might want to be bigger, consider |
367 | /// wrapping it in a [SingleChildScrollView] or [OverflowBox]. |
368 | /// |
369 | /// {@tool dartpad} |
370 | /// This example uses a [LayoutBuilder] to build a different widget depending on the available width. Resize the |
371 | /// DartPad window to see [LayoutBuilder] in action! |
372 | /// |
373 | /// ** See code in examples/api/lib/widgets/layout_builder/layout_builder.0.dart ** |
374 | /// {@end-tool} |
375 | /// |
376 | /// See also: |
377 | /// |
378 | /// * [SliverLayoutBuilder], the sliver counterpart of this widget. |
379 | /// * [Builder], which calls a `builder` function at build time. |
380 | /// * [StatefulBuilder], which passes its `builder` function a `setState` callback. |
381 | /// * [CustomSingleChildLayout], which positions its child during layout. |
382 | /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). |
383 | class LayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> { |
384 | /// Creates a widget that defers its building until layout. |
385 | const LayoutBuilder({super.key, required super.builder}); |
386 | |
387 | @override |
388 | RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox> createRenderObject( |
389 | BuildContext context, |
390 | ) => _RenderLayoutBuilder(); |
391 | } |
392 | |
393 | class _RenderLayoutBuilder extends RenderBox |
394 | with |
395 | RenderObjectWithChildMixin<RenderBox>, |
396 | RenderObjectWithLayoutCallbackMixin, |
397 | RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox> { |
398 | @override |
399 | double computeMinIntrinsicWidth(double height) { |
400 | assert(_debugThrowIfNotCheckingIntrinsics()); |
401 | return 0.0; |
402 | } |
403 | |
404 | @override |
405 | double computeMaxIntrinsicWidth(double height) { |
406 | assert(_debugThrowIfNotCheckingIntrinsics()); |
407 | return 0.0; |
408 | } |
409 | |
410 | @override |
411 | double computeMinIntrinsicHeight(double width) { |
412 | assert(_debugThrowIfNotCheckingIntrinsics()); |
413 | return 0.0; |
414 | } |
415 | |
416 | @override |
417 | double computeMaxIntrinsicHeight(double width) { |
418 | assert(_debugThrowIfNotCheckingIntrinsics()); |
419 | return 0.0; |
420 | } |
421 | |
422 | @override |
423 | Size computeDryLayout(BoxConstraints constraints) { |
424 | assert( |
425 | debugCannotComputeDryLayout( |
426 | reason: |
427 | 'Calculating the dry layout would require running the layout callback ' |
428 | 'speculatively, which might mutate the live render object tree.', |
429 | ), |
430 | ); |
431 | return Size.zero; |
432 | } |
433 | |
434 | @override |
435 | double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) { |
436 | assert( |
437 | debugCannotComputeDryLayout( |
438 | reason: |
439 | 'Calculating the dry baseline would require running the layout callback ' |
440 | 'speculatively, which might mutate the live render object tree.', |
441 | ), |
442 | ); |
443 | return null; |
444 | } |
445 | |
446 | @override |
447 | void performLayout() { |
448 | final BoxConstraints constraints = this.constraints; |
449 | runLayoutCallback(); |
450 | if (child != null) { |
451 | child!.layout(constraints, parentUsesSize: true); |
452 | size = constraints.constrain(child!.size); |
453 | } else { |
454 | size = constraints.biggest; |
455 | } |
456 | } |
457 | |
458 | @override |
459 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
460 | return child?.getDistanceToActualBaseline(baseline) ?? |
461 | super.computeDistanceToActualBaseline(baseline); |
462 | } |
463 | |
464 | @override |
465 | bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { |
466 | return child?.hitTest(result, position: position) ?? false; |
467 | } |
468 | |
469 | @override |
470 | void paint(PaintingContext context, Offset offset) { |
471 | if (child != null) { |
472 | context.paintChild(child!, offset); |
473 | } |
474 | } |
475 | |
476 | bool _debugThrowIfNotCheckingIntrinsics() { |
477 | assert(() { |
478 | if (!RenderObject.debugCheckingIntrinsics) { |
479 | throw FlutterError( |
480 | 'LayoutBuilder does not support returning intrinsic dimensions.\n' |
481 | 'Calculating the intrinsic dimensions would require running the layout ' |
482 | 'callback speculatively, which might mutate the live render object tree.', |
483 | ); |
484 | } |
485 | return true; |
486 | }()); |
487 | |
488 | return true; |
489 | } |
490 | } |
491 | |
492 | FlutterErrorDetails _reportException( |
493 | DiagnosticsNode context, |
494 | Object exception, |
495 | StackTrace stack, { |
496 | InformationCollector? informationCollector, |
497 | }) { |
498 | final FlutterErrorDetails details = FlutterErrorDetails( |
499 | exception: exception, |
500 | stack: stack, |
501 | library: 'widgets library', |
502 | context: context, |
503 | informationCollector: informationCollector, |
504 | ); |
505 | FlutterError.reportError(details); |
506 | return details; |
507 | } |
508 |
Definitions
- AbstractLayoutBuilder
- AbstractLayoutBuilder
- builder
- createElement
- updateShouldRebuild
- createRenderObject
- ConstrainedLayoutBuilder
- ConstrainedLayoutBuilder
- _LayoutBuilderElement
- _LayoutBuilderElement
- renderObject
- buildScope
- _scheduleRebuild
- _frameCallback
- visitChildren
- forgetChild
- mount
- update
- markNeedsBuild
- performRebuild
- unmount
- _rebuildWithConstraints
- updateChildCallback
- insertRenderObjectChild
- moveRenderObjectChild
- removeRenderObjectChild
- RenderAbstractLayoutBuilderMixin
- _updateCallback
- layoutCallback
- layoutInfo
- LayoutBuilder
- LayoutBuilder
- createRenderObject
- _RenderLayoutBuilder
- computeMinIntrinsicWidth
- computeMaxIntrinsicWidth
- computeMinIntrinsicHeight
- computeMaxIntrinsicHeight
- computeDryLayout
- computeDryBaseline
- performLayout
- computeDistanceToActualBaseline
- hitTestChildren
- paint
- _debugThrowIfNotCheckingIntrinsics
Learn more about Flutter for embedded and desktop on industrialflutter.com