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