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 'package:flutter/material.dart'; |
6 | /// @docImport 'package:flutter/rendering.dart'; |
7 | /// @docImport 'package:flutter/scheduler.dart'; |
8 | /// |
9 | /// @docImport 'binding.dart'; |
10 | /// @docImport 'widget_inspector.dart'; |
11 | library; |
12 | |
13 | import 'dart:collection'; |
14 | import 'dart:developer' show Timeline; // to disambiguate reference in dartdocs below |
15 | |
16 | import 'package:flutter/foundation.dart'; |
17 | |
18 | import 'basic.dart'; |
19 | import 'framework.dart'; |
20 | import 'localizations.dart'; |
21 | import 'lookup_boundary.dart'; |
22 | import 'media_query.dart'; |
23 | import 'overlay.dart'; |
24 | import 'table.dart'; |
25 | |
26 | // Examples can assume: |
27 | // late BuildContext context; |
28 | // List children = []; |
29 | // List items = []; |
30 | |
31 | // Any changes to this file should be reflected in the debugAssertAllWidgetVarsUnset() |
32 | // function below. |
33 | |
34 | /// Log the dirty widgets that are built each frame. |
35 | /// |
36 | /// Combined with [debugPrintBuildScope] or [debugPrintBeginFrameBanner], this |
37 | /// allows you to distinguish builds triggered by the initial mounting of a |
38 | /// widget tree (e.g. in a call to [runApp]) from the regular builds triggered |
39 | /// by the pipeline. |
40 | /// |
41 | /// Combined with [debugPrintScheduleBuildForStacks], this lets you watch a |
42 | /// widget's dirty/clean lifecycle. |
43 | /// |
44 | /// To get similar information but showing it on the timeline available from |
45 | /// Flutter DevTools rather than getting it in the console (where it can be |
46 | /// overwhelming), consider [debugProfileBuildsEnabled]. |
47 | /// |
48 | /// See also: |
49 | /// |
50 | /// * [WidgetsBinding.drawFrame], which pumps the build and rendering pipeline |
51 | /// to generate a frame. |
52 | bool debugPrintRebuildDirtyWidgets = false; |
53 | |
54 | /// Signature for [debugOnRebuildDirtyWidget] implementations. |
55 | typedef RebuildDirtyWidgetCallback = void Function(Element e, bool builtOnce); |
56 | |
57 | /// Callback invoked for every dirty widget built each frame. |
58 | /// |
59 | /// This callback is only invoked in debug builds. |
60 | /// |
61 | /// See also: |
62 | /// |
63 | /// * [debugPrintRebuildDirtyWidgets], which does something similar but logs |
64 | /// to the console instead of invoking a callback. |
65 | /// * [debugOnProfilePaint], which does something similar for [RenderObject] |
66 | /// painting. |
67 | /// * [WidgetInspectorService], which uses the [debugOnRebuildDirtyWidget] |
68 | /// callback to generate aggregate profile statistics describing which widget |
69 | /// rebuilds occurred when the |
70 | /// `ext.flutter.inspector.trackRebuildDirtyWidgets` service extension is |
71 | /// enabled. |
72 | RebuildDirtyWidgetCallback? debugOnRebuildDirtyWidget; |
73 | |
74 | /// Log all calls to [BuildOwner.buildScope]. |
75 | /// |
76 | /// Combined with [debugPrintScheduleBuildForStacks], this allows you to track |
77 | /// when a [State.setState] call gets serviced. |
78 | /// |
79 | /// Combined with [debugPrintRebuildDirtyWidgets] or |
80 | /// [debugPrintBeginFrameBanner], this allows you to distinguish builds |
81 | /// triggered by the initial mounting of a widget tree (e.g. in a call to |
82 | /// [runApp]) from the regular builds triggered by the pipeline. |
83 | /// |
84 | /// See also: |
85 | /// |
86 | /// * [WidgetsBinding.drawFrame], which pumps the build and rendering pipeline |
87 | /// to generate a frame. |
88 | bool debugPrintBuildScope = false; |
89 | |
90 | /// Log the call stacks that mark widgets as needing to be rebuilt. |
91 | /// |
92 | /// This is called whenever [BuildOwner.scheduleBuildFor] adds an element to the |
93 | /// dirty list. Typically this is as a result of [Element.markNeedsBuild] being |
94 | /// called, which itself is usually a result of [State.setState] being called. |
95 | /// |
96 | /// To see when a widget is rebuilt, see [debugPrintRebuildDirtyWidgets]. |
97 | /// |
98 | /// To see when the dirty list is flushed, see [debugPrintBuildScope]. |
99 | /// |
100 | /// To see when a frame is scheduled, see [debugPrintScheduleFrameStacks]. |
101 | bool debugPrintScheduleBuildForStacks = false; |
102 | |
103 | /// Log when widgets with global keys are deactivated and log when they are |
104 | /// reactivated (retaken). |
105 | /// |
106 | /// This can help track down framework bugs relating to the [GlobalKey] logic. |
107 | bool debugPrintGlobalKeyedWidgetLifecycle = false; |
108 | |
109 | /// Adds [Timeline] events for every Widget built. |
110 | /// |
111 | /// The timing information this flag exposes is not representative of the actual |
112 | /// cost of building, because the overhead of adding timeline events is |
113 | /// significant relative to the time each object takes to build. However, it can |
114 | /// expose unexpected widget behavior in the timeline. |
115 | /// |
116 | /// In debug builds, additional information is included in the trace (such as |
117 | /// the properties of widgets being built). Collecting this data is |
118 | /// expensive and further makes these traces non-representative of actual |
119 | /// performance. This data is omitted in profile builds. |
120 | /// |
121 | /// For more information about performance debugging in Flutter, see |
122 | /// <https://docs.flutter.dev/perf/ui-performance>. |
123 | /// |
124 | /// See also: |
125 | /// |
126 | /// * [debugPrintRebuildDirtyWidgets], which does something similar but |
127 | /// reporting the builds to the console. |
128 | /// * [debugProfileLayoutsEnabled], which does something similar for layout, |
129 | /// and [debugPrintLayouts], its console equivalent. |
130 | /// * [debugProfilePaintsEnabled], which does something similar for painting. |
131 | /// * [debugProfileBuildsEnabledUserWidgets], which adds events for user-created |
132 | /// [Widget] build times and incurs less overhead. |
133 | /// * [debugEnhanceBuildTimelineArguments], which enhances the trace with |
134 | /// debugging information related to [Widget] builds. |
135 | bool debugProfileBuildsEnabled = false; |
136 | |
137 | /// Adds [Timeline] events for every user-created [Widget] built. |
138 | /// |
139 | /// A user-created [Widget] is any [Widget] that is constructed in the root |
140 | /// library. Often [Widget]s contain child [Widget]s that are constructed in |
141 | /// libraries (for example, a [TextButton] having a [RichText] child). Timeline |
142 | /// events for those children will be omitted with this flag. This works for any |
143 | /// [Widget] not just ones declared in the root library. |
144 | /// |
145 | /// See also: |
146 | /// |
147 | /// * [debugProfileBuildsEnabled], which functions similarly but shows events |
148 | /// for every widget and has a higher overhead cost. |
149 | /// * [debugEnhanceBuildTimelineArguments], which enhances the trace with |
150 | /// debugging information related to [Widget] builds. |
151 | bool debugProfileBuildsEnabledUserWidgets = false; |
152 | |
153 | /// Adds debugging information to [Timeline] events related to [Widget] builds. |
154 | /// |
155 | /// This flag will only add [Timeline] event arguments for debug builds. |
156 | /// Additional arguments will be added for the "BUILD" [Timeline] event and for |
157 | /// all [Widget] build [Timeline] events, which are the [Timeline] events that |
158 | /// are added when either of [debugProfileBuildsEnabled] and |
159 | /// [debugProfileBuildsEnabledUserWidgets] are true. The debugging information |
160 | /// that will be added in trace arguments includes stats around [Widget] dirty |
161 | /// states and [Widget] diagnostic information (i.e. [Widget] properties). |
162 | /// |
163 | /// See also: |
164 | /// |
165 | /// * [debugProfileBuildsEnabled], which adds [Timeline] events for every |
166 | /// [Widget] built. |
167 | /// * [debugProfileBuildsEnabledUserWidgets], which adds [Timeline] events for |
168 | /// every user-created [Widget] built. |
169 | /// * [debugEnhanceLayoutTimelineArguments], which does something similar for |
170 | /// events related to [RenderObject] layouts. |
171 | /// * [debugEnhancePaintTimelineArguments], which does something similar for |
172 | /// events related to [RenderObject] paints. |
173 | bool debugEnhanceBuildTimelineArguments = false; |
174 | |
175 | /// Show banners for deprecated widgets. |
176 | bool debugHighlightDeprecatedWidgets = false; |
177 | |
178 | Key? _firstNonUniqueKey(Iterable<Widget> widgets) { |
179 | final Set<Key> keySet = HashSet<Key>(); |
180 | for (final Widget widget in widgets) { |
181 | if (widget.key == null) { |
182 | continue; |
183 | } |
184 | if (!keySet.add(widget.key!)) { |
185 | return widget.key; |
186 | } |
187 | } |
188 | return null; |
189 | } |
190 | |
191 | /// Asserts if the given child list contains any duplicate non-null keys. |
192 | /// |
193 | /// To invoke this function, use the following pattern: |
194 | /// |
195 | /// ```dart |
196 | /// class MyWidget extends StatelessWidget { |
197 | /// MyWidget({ super.key, required this.children }) { |
198 | /// assert(!debugChildrenHaveDuplicateKeys(this, children)); |
199 | /// } |
200 | /// |
201 | /// final List<Widget> children; |
202 | /// |
203 | /// // ... |
204 | /// } |
205 | /// ``` |
206 | /// |
207 | /// If specified, the `message` overrides the default message. |
208 | /// |
209 | /// For a version of this function that can be used in contexts where |
210 | /// the list of items does not have a particular parent, see |
211 | /// [debugItemsHaveDuplicateKeys]. |
212 | /// |
213 | /// Does nothing if asserts are disabled. Always returns false. |
214 | bool debugChildrenHaveDuplicateKeys(Widget parent, Iterable<Widget> children, { String? message }) { |
215 | assert(() { |
216 | final Key? nonUniqueKey = _firstNonUniqueKey(children); |
217 | if (nonUniqueKey != null) { |
218 | throw FlutterError( |
219 | " ${message ?? 'Duplicate keys found.\n' |
220 | 'If multiple keyed widgets exist as children of another widget, they must have unique keys.' }" |
221 | '\n $parent has multiple children with key $nonUniqueKey.' , |
222 | ); |
223 | } |
224 | return true; |
225 | }()); |
226 | return false; |
227 | } |
228 | |
229 | /// Asserts if the given list of items contains any duplicate non-null keys. |
230 | /// |
231 | /// To invoke this function, use the following pattern: |
232 | /// |
233 | /// ```dart |
234 | /// assert(!debugItemsHaveDuplicateKeys(items)); |
235 | /// ``` |
236 | /// |
237 | /// For a version of this function specifically intended for parents |
238 | /// checking their children lists, see [debugChildrenHaveDuplicateKeys]. |
239 | /// |
240 | /// Does nothing if asserts are disabled. Always returns false. |
241 | bool debugItemsHaveDuplicateKeys(Iterable<Widget> items) { |
242 | assert(() { |
243 | final Key? nonUniqueKey = _firstNonUniqueKey(items); |
244 | if (nonUniqueKey != null) { |
245 | throw FlutterError('Duplicate key found: $nonUniqueKey.' ); |
246 | } |
247 | return true; |
248 | }()); |
249 | return false; |
250 | } |
251 | |
252 | /// Asserts that the given context has a [Table] ancestor. |
253 | /// |
254 | /// Used by [TableRowInkWell] to make sure that it is only used in an appropriate context. |
255 | /// |
256 | /// To invoke this function, use the following pattern, typically in the |
257 | /// relevant Widget's build method: |
258 | /// |
259 | /// ```dart |
260 | /// assert(debugCheckHasTable(context)); |
261 | /// ``` |
262 | /// |
263 | /// Always place this before any early returns, so that the invariant is checked |
264 | /// in all cases. This prevents bugs from hiding until a particular codepath is |
265 | /// hit. |
266 | /// |
267 | /// This method can be expensive (it walks the element tree). |
268 | /// |
269 | /// Does nothing if asserts are disabled. Always returns true. |
270 | bool debugCheckHasTable(BuildContext context) { |
271 | assert(() { |
272 | if (context.widget is! Table && context.findAncestorWidgetOfExactType<Table>() == null) { |
273 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
274 | ErrorSummary('No Table widget found.' ), |
275 | ErrorDescription(' ${context.widget.runtimeType} widgets require a Table widget ancestor.' ), |
276 | context.describeWidget('The specific widget that could not find a Table ancestor was' ), |
277 | context.describeOwnershipChain('The ownership chain for the affected widget is' ), |
278 | ]); |
279 | } |
280 | return true; |
281 | }()); |
282 | return true; |
283 | } |
284 | |
285 | /// Asserts that the given context has a [MediaQuery] ancestor. |
286 | /// |
287 | /// Used by various widgets to make sure that they are only used in an |
288 | /// appropriate context. |
289 | /// |
290 | /// To invoke this function, use the following pattern, typically in the |
291 | /// relevant Widget's build method: |
292 | /// |
293 | /// ```dart |
294 | /// assert(debugCheckHasMediaQuery(context)); |
295 | /// ``` |
296 | /// |
297 | /// Always place this before any early returns, so that the invariant is checked |
298 | /// in all cases. This prevents bugs from hiding until a particular codepath is |
299 | /// hit. |
300 | /// |
301 | /// Does nothing if asserts are disabled. Always returns true. |
302 | bool debugCheckHasMediaQuery(BuildContext context) { |
303 | assert(() { |
304 | if (context.widget is! MediaQuery && context.getElementForInheritedWidgetOfExactType<MediaQuery>() == null) { |
305 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
306 | ErrorSummary('No MediaQuery widget ancestor found.' ), |
307 | ErrorDescription(' ${context.widget.runtimeType} widgets require a MediaQuery widget ancestor.' ), |
308 | context.describeWidget('The specific widget that could not find a MediaQuery ancestor was' ), |
309 | context.describeOwnershipChain('The ownership chain for the affected widget is' ), |
310 | ErrorHint( |
311 | 'No MediaQuery ancestor could be found starting from the context ' |
312 | 'that was passed to MediaQuery.of(). This can happen because the ' |
313 | 'context used is not a descendant of a View widget, which introduces ' |
314 | 'a MediaQuery.' |
315 | ), |
316 | ]); |
317 | } |
318 | return true; |
319 | }()); |
320 | return true; |
321 | } |
322 | |
323 | /// Asserts that the given context has a [Directionality] ancestor. |
324 | /// |
325 | /// Used by various widgets to make sure that they are only used in an |
326 | /// appropriate context. |
327 | /// |
328 | /// To invoke this function, use the following pattern, typically in the |
329 | /// relevant Widget's build method: |
330 | /// |
331 | /// ```dart |
332 | /// assert(debugCheckHasDirectionality(context)); |
333 | /// ``` |
334 | /// |
335 | /// To improve the error messages you can add some extra color using the |
336 | /// named arguments. |
337 | /// |
338 | /// * why: explain why the direction is needed, for example "to resolve |
339 | /// the 'alignment' argument". Should be an adverb phrase describing why. |
340 | /// * hint: explain why this might be happening, for example "The default |
341 | /// value of the 'alignment' argument of the $runtimeType widget is an |
342 | /// AlignmentDirectional value.". Should be a fully punctuated sentence. |
343 | /// * alternative: provide additional advice specific to the situation, |
344 | /// especially an alternative to providing a Directionality ancestor. |
345 | /// For example, "Alternatively, consider specifying the 'textDirection' |
346 | /// argument.". Should be a fully punctuated sentence. |
347 | /// |
348 | /// Each one can be null, in which case it is skipped (this is the default). |
349 | /// If they are non-null, they are included in the order above, interspersed |
350 | /// with the more generic advice regarding [Directionality]. |
351 | /// |
352 | /// Always place this before any early returns, so that the invariant is checked |
353 | /// in all cases. This prevents bugs from hiding until a particular codepath is |
354 | /// hit. |
355 | /// |
356 | /// Does nothing if asserts are disabled. Always returns true. |
357 | bool debugCheckHasDirectionality(BuildContext context, { String? why, String? hint, String? alternative }) { |
358 | assert(() { |
359 | if (context.widget is! Directionality && context.getElementForInheritedWidgetOfExactType<Directionality>() == null) { |
360 | why = why == null ? '' : ' $why' ; |
361 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
362 | ErrorSummary('No Directionality widget found.' ), |
363 | ErrorDescription(' ${context.widget.runtimeType} widgets require a Directionality widget ancestor $why.\n' ), |
364 | if (hint != null) |
365 | ErrorHint(hint), |
366 | context.describeWidget('The specific widget that could not find a Directionality ancestor was' ), |
367 | context.describeOwnershipChain('The ownership chain for the affected widget is' ), |
368 | ErrorHint( |
369 | 'Typically, the Directionality widget is introduced by the MaterialApp ' |
370 | 'or WidgetsApp widget at the top of your application widget tree. It ' |
371 | 'determines the ambient reading direction and is used, for example, to ' |
372 | 'determine how to lay out text, how to interpret "start" and "end" ' |
373 | 'values, and to resolve EdgeInsetsDirectional, ' |
374 | 'AlignmentDirectional, and other *Directional objects.' , |
375 | ), |
376 | if (alternative != null) |
377 | ErrorHint(alternative), |
378 | ]); |
379 | } |
380 | return true; |
381 | }()); |
382 | return true; |
383 | } |
384 | |
385 | /// Asserts that the `built` widget is not null. |
386 | /// |
387 | /// Used when the given `widget` calls a builder function to check that the |
388 | /// function returned a non-null value, as typically required. |
389 | /// |
390 | /// Does nothing when asserts are disabled. |
391 | void debugWidgetBuilderValue(Widget widget, Widget? built) { |
392 | assert(() { |
393 | if (built == null) { |
394 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
395 | ErrorSummary('A build function returned null.' ), |
396 | DiagnosticsProperty<Widget>('The offending widget is' , widget, style: DiagnosticsTreeStyle.errorProperty), |
397 | ErrorDescription('Build functions must never return null.' ), |
398 | ErrorHint( |
399 | 'To return an empty space that causes the building widget to fill available room, return "Container()". ' |
400 | 'To return an empty space that takes as little room as possible, return "Container(width: 0.0, height: 0.0)".' , |
401 | ), |
402 | ]); |
403 | } |
404 | if (widget == built) { |
405 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
406 | ErrorSummary('A build function returned context.widget.' ), |
407 | DiagnosticsProperty<Widget>('The offending widget is' , widget, style: DiagnosticsTreeStyle.errorProperty), |
408 | ErrorDescription( |
409 | 'Build functions must never return their BuildContext parameter\'s widget or a child that contains "context.widget". ' |
410 | 'Doing so introduces a loop in the widget tree that can cause the app to crash.' , |
411 | ), |
412 | ]); |
413 | } |
414 | return true; |
415 | }()); |
416 | } |
417 | |
418 | /// Asserts that the given context has a [Localizations] ancestor that contains |
419 | /// a [WidgetsLocalizations] delegate. |
420 | /// |
421 | /// To call this function, use the following pattern, typically in the |
422 | /// relevant Widget's build method: |
423 | /// |
424 | /// ```dart |
425 | /// assert(debugCheckHasWidgetsLocalizations(context)); |
426 | /// ``` |
427 | /// |
428 | /// Always place this before any early returns, so that the invariant is checked |
429 | /// in all cases. This prevents bugs from hiding until a particular codepath is |
430 | /// hit. |
431 | /// |
432 | /// Does nothing if asserts are disabled. Always returns true. |
433 | bool debugCheckHasWidgetsLocalizations(BuildContext context) { |
434 | assert(() { |
435 | if (Localizations.of<WidgetsLocalizations>(context, WidgetsLocalizations) == null) { |
436 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
437 | ErrorSummary('No WidgetsLocalizations found.' ), |
438 | ErrorDescription( |
439 | ' ${context.widget.runtimeType} widgets require WidgetsLocalizations ' |
440 | 'to be provided by a Localizations widget ancestor.' , |
441 | ), |
442 | ErrorDescription( |
443 | 'The widgets library uses Localizations to generate messages, ' |
444 | 'labels, and abbreviations.' , |
445 | ), |
446 | ErrorHint( |
447 | 'To introduce a WidgetsLocalizations, either use a ' |
448 | 'WidgetsApp at the root of your application to include them ' |
449 | 'automatically, or add a Localization widget with a ' |
450 | 'WidgetsLocalizations delegate.' , |
451 | ), |
452 | ...context.describeMissingAncestor(expectedAncestorType: WidgetsLocalizations), |
453 | ]); |
454 | } |
455 | return true; |
456 | }()); |
457 | return true; |
458 | } |
459 | |
460 | /// Asserts that the given context has an [Overlay] ancestor. |
461 | /// |
462 | /// To call this function, use the following pattern, typically in the |
463 | /// relevant Widget's build method: |
464 | /// |
465 | /// ```dart |
466 | /// assert(debugCheckHasOverlay(context)); |
467 | /// ``` |
468 | /// |
469 | /// Always place this before any early returns, so that the invariant is checked |
470 | /// in all cases. This prevents bugs from hiding until a particular codepath is |
471 | /// hit. |
472 | /// |
473 | /// This method can be expensive (it walks the element tree). |
474 | /// |
475 | /// Does nothing if asserts are disabled. Always returns true. |
476 | bool debugCheckHasOverlay(BuildContext context) { |
477 | assert(() { |
478 | if (LookupBoundary.findAncestorWidgetOfExactType<Overlay>(context) == null) { |
479 | final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Overlay>(context); |
480 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
481 | ErrorSummary('No Overlay widget found ${hiddenByBoundary ? ' within the closest LookupBoundary' : '' }.' ), |
482 | if (hiddenByBoundary) |
483 | ErrorDescription( |
484 | 'There is an ancestor Overlay widget, but it is hidden by a LookupBoundary.' |
485 | ), |
486 | ErrorDescription( |
487 | ' ${context.widget.runtimeType} widgets require an Overlay ' |
488 | 'widget ancestor within the closest LookupBoundary.\n' |
489 | 'An overlay lets widgets float on top of other widget children.' , |
490 | ), |
491 | ErrorHint( |
492 | 'To introduce an Overlay widget, you can either directly ' |
493 | 'include one, or use a widget that contains an Overlay itself, ' |
494 | 'such as a Navigator, WidgetApp, MaterialApp, or CupertinoApp.' , |
495 | ), |
496 | ...context.describeMissingAncestor(expectedAncestorType: Overlay), |
497 | ]); |
498 | } |
499 | return true; |
500 | }()); |
501 | return true; |
502 | } |
503 | |
504 | /// Returns true if none of the widget library debug variables have been changed. |
505 | /// |
506 | /// This function is used by the test framework to ensure that debug variables |
507 | /// haven't been inadvertently changed. |
508 | /// |
509 | /// See [the widgets library](widgets/widgets-library.html) for a complete list. |
510 | bool debugAssertAllWidgetVarsUnset(String reason) { |
511 | assert(() { |
512 | if (debugPrintRebuildDirtyWidgets || |
513 | debugPrintBuildScope || |
514 | debugPrintScheduleBuildForStacks || |
515 | debugPrintGlobalKeyedWidgetLifecycle || |
516 | debugProfileBuildsEnabled || |
517 | debugHighlightDeprecatedWidgets || |
518 | debugProfileBuildsEnabledUserWidgets) { |
519 | throw FlutterError(reason); |
520 | } |
521 | return true; |
522 | }()); |
523 | return true; |
524 | } |
525 | |