debug.dart 11.9 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:collection';
6
import 'dart:developer' show Timeline; // to disambiguate reference in dartdocs below
7

8 9
import 'package:flutter/foundation.dart';

10
import 'basic.dart';
11
import 'framework.dart';
12
import 'media_query.dart';
13
import 'table.dart';
14

15 16 17
// Any changes to this file should be reflected in the debugAssertAllWidgetVarsUnset()
// function below.

18
/// Log the dirty widgets that are built each frame.
19 20 21 22
///
/// Combined with [debugPrintBuildScope] or [debugPrintBeginFrameBanner], this
/// allows you to distinguish builds triggered by the initial mounting of a
/// widget tree (e.g. in a call to [runApp]) from the regular builds triggered
23
/// by the pipeline.
24 25 26
///
/// Combined with [debugPrintScheduleBuildForStacks], this lets you watch a
/// widget's dirty/clean lifecycle.
27
///
28 29 30 31
/// To get similar information but showing it on the timeline available from the
/// Observatory rather than getting it in the console (where it can be
/// overwhelming), consider [debugProfileBuildsEnabled].
///
32
/// See also the discussion at [WidgetsBinding.drawFrame].
33 34
bool debugPrintRebuildDirtyWidgets = false;

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
/// Signature for [debugOnRebuildDirtyWidget] implementations.
typedef RebuildDirtyWidgetCallback = void Function(Element e, bool builtOnce);

/// Callback invoked for every dirty widget built each frame.
///
/// This callback is only invoked in debug builds.
///
/// See also:
///
///  * [debugPrintRebuildDirtyWidgets], which does something similar but logs
///    to the console instead of invoking a callback.
///  * [debugOnProfilePaint], which does something similar for [RenderObject]
///    painting.
///  * [WidgetInspectorService], which uses the [debugOnRebuildDirtyWidget]
///    callback to generate aggregate profile statistics describing which widget
///    rebuilds occurred when the
///    `ext.flutter.inspector.trackRebuildDirtyWidgets` service extension is
///    enabled.
RebuildDirtyWidgetCallback debugOnRebuildDirtyWidget;

55 56 57 58 59 60 61 62
/// Log all calls to [BuildOwner.buildScope].
///
/// Combined with [debugPrintScheduleBuildForStacks], this allows you to track
/// when a [State.setState] call gets serviced.
///
/// Combined with [debugPrintRebuildDirtyWidgets] or
/// [debugPrintBeginFrameBanner], this allows you to distinguish builds
/// triggered by the initial mounting of a widget tree (e.g. in a call to
63 64 65
/// [runApp]) from the regular builds triggered by the pipeline.
///
/// See also the discussion at [WidgetsBinding.drawFrame].
66 67 68 69 70 71 72 73 74 75
bool debugPrintBuildScope = false;

/// Log the call stacks that mark widgets as needing to be rebuilt.
///
/// This is called whenever [BuildOwner.scheduleBuildFor] adds an element to the
/// dirty list. Typically this is as a result of [Element.markNeedsBuild] being
/// called, which itself is usually a result of [State.setState] being called.
///
/// To see when a widget is rebuilt, see [debugPrintRebuildDirtyWidgets].
///
76
/// To see when the dirty list is flushed, see [debugPrintBuildScope].
77 78
///
/// To see when a frame is scheduled, see [debugPrintScheduleFrameStacks].
79 80
bool debugPrintScheduleBuildForStacks = false;

81 82 83 84 85 86
/// Log when widgets with global keys are deactivated and log when they are
/// reactivated (retaken).
///
/// This can help track down framework bugs relating to the [GlobalKey] logic.
bool debugPrintGlobalKeyedWidgetLifecycle = false;

87 88 89
/// Adds [Timeline] events for every Widget built.
///
/// For details on how to use [Timeline] events in the Dart Observatory to
90
/// optimize your app, see https://flutter.dev/docs/testing/debugging#tracing-any-dart-code-performance
91
/// and https://fuchsia.googlesource.com/topaz/+/master/shell/docs/performance.md
92 93 94 95
///
/// See also [debugProfilePaintsEnabled], which does something similar but for
/// painting, and [debugPrintRebuildDirtyWidgets], which does something similar
/// but reporting the builds to the console.
96 97
bool debugProfileBuildsEnabled = false;

98 99 100
/// Show banners for deprecated widgets.
bool debugHighlightDeprecatedWidgets = false;

101
Key _firstNonUniqueKey(Iterable<Widget> widgets) {
102
  final Set<Key> keySet = HashSet<Key>();
103 104 105 106 107 108 109 110 111 112
  for (Widget widget in widgets) {
    assert(widget != null);
    if (widget.key == null)
      continue;
    if (!keySet.add(widget.key))
      return widget.key;
  }
  return null;
}

113 114 115 116 117 118 119 120 121 122 123 124 125 126
/// Asserts if the given child list contains any duplicate non-null keys.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's constructor:
///
/// ```dart
/// assert(!debugChildrenHaveDuplicateKeys(this, children));
/// ```
///
/// For a version of this function that can be used in contexts where
/// the list of items does not have a particular parent, see
/// [debugItemsHaveDuplicateKeys].
///
/// Does nothing if asserts are disabled. Always returns true.
127
bool debugChildrenHaveDuplicateKeys(Widget parent, Iterable<Widget> children) {
128
  assert(() {
129 130
    final Key nonUniqueKey = _firstNonUniqueKey(children);
    if (nonUniqueKey != null) {
131 132 133 134 135 136 137
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('Duplicate keys found.'),
        ErrorDescription(
          'If multiple keyed nodes exist as children of another node, they must have unique keys.\n'
          '$parent has multiple children with key $nonUniqueKey.'
        ),
      ]);
138 139
    }
    return true;
140
  }());
141 142
  return false;
}
143

144 145 146 147 148 149 150 151 152 153 154 155
/// Asserts if the given list of items contains any duplicate non-null keys.
///
/// To invoke this function, use the following pattern:
///
/// ```dart
/// assert(!debugItemsHaveDuplicateKeys(items));
/// ```
///
/// For a version of this function specifically intended for parents
/// checking their children lists, see [debugChildrenHaveDuplicateKeys].
///
/// Does nothing if asserts are disabled. Always returns true.
156 157 158 159
bool debugItemsHaveDuplicateKeys(Iterable<Widget> items) {
  assert(() {
    final Key nonUniqueKey = _firstNonUniqueKey(items);
    if (nonUniqueKey != null)
160 161 162
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('Duplicate key found: $nonUniqueKey.'),
      ]);
163
    return true;
164
  }());
165 166
  return false;
}
167 168 169

/// Asserts that the given context has a [Table] ancestor.
///
170
/// Used by [TableRowInkWell] to make sure that it is only used in an appropriate context.
171 172
///
/// To invoke this function, use the following pattern, typically in the
173
/// relevant Widget's build method:
174 175 176 177 178 179 180 181 182
///
/// ```dart
/// assert(debugCheckHasTable(context));
/// ```
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasTable(BuildContext context) {
  assert(() {
    if (context.widget is! Table && context.ancestorWidgetOfExactType(Table) == null) {
183 184 185 186 187 188
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('No Table widget found.'),
        ErrorDescription('${context.widget.runtimeType} widgets require a Table widget ancestor.'),
        context.describeWidget('The specific widget that could not find a Table ancestor was'),
        context.describeOwnershipChain('The ownership chain for the affected widget is'),
      ]);
189 190
    }
    return true;
191
  }());
192 193
  return true;
}
194

195 196 197 198 199 200
/// Asserts that the given context has a [MediaQuery] ancestor.
///
/// Used by various widgets to make sure that they are only used in an
/// appropriate context.
///
/// To invoke this function, use the following pattern, typically in the
201
/// relevant Widget's build method:
202 203 204 205 206 207 208 209 210
///
/// ```dart
/// assert(debugCheckHasMediaQuery(context));
/// ```
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasMediaQuery(BuildContext context) {
  assert(() {
    if (context.widget is! MediaQuery && context.ancestorWidgetOfExactType(MediaQuery) == null) {
211 212 213 214 215 216 217 218 219 220
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('No MediaQuery widget found.'),
        ErrorDescription('${context.widget.runtimeType} widgets require a MediaQuery widget ancestor.'),
        context.describeWidget('The specific widget that could not find a MediaQuery ancestor was'),
        context.describeOwnershipChain('The ownership chain for the affected widget is'),
        ErrorHint(
          'Typically, the MediaQuery widget is introduced by the MaterialApp or '
          'WidgetsApp widget at the top of your application widget tree.'
        ),
      ]);
221 222
    }
    return true;
223
  }());
224 225 226
  return true;
}

227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
/// Asserts that the given context has a [Directionality] ancestor.
///
/// Used by various widgets to make sure that they are only used in an
/// appropriate context.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's build method:
///
/// ```dart
/// assert(debugCheckHasDirectionality(context));
/// ```
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasDirectionality(BuildContext context) {
  assert(() {
    if (context.widget is! Directionality && context.ancestorWidgetOfExactType(Directionality) == null) {
243 244 245 246 247 248 249 250 251 252 253 254 255 256
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('No Directionality widget found.'),
        ErrorDescription('${context.widget.runtimeType} widgets require a Directionality widget ancestor.\n'),
        context.describeWidget('The specific widget that could not find a Directionality ancestor was'),
        context.describeOwnershipChain('The ownership chain for the affected widget is'),
        ErrorHint(
          'Typically, the Directionality widget is introduced by the MaterialApp '
          'or WidgetsApp widget at the top of your application widget tree. It '
          'determines the ambient reading direction and is used, for example, to '
          'determine how to lay out text, how to interpret "start" and "end" '
          'values, and to resolve EdgeInsetsDirectional, '
          'AlignmentDirectional, and other *Directional objects.'
        ),
      ]);
257 258 259 260 261 262
    }
    return true;
  }());
  return true;
}

263 264 265 266 267 268
/// Asserts that the `built` widget is not null.
///
/// Used when the given `widget` calls a builder function to check that the
/// function returned a non-null value, as typically required.
///
/// Does nothing when asserts are disabled.
269 270 271
void debugWidgetBuilderValue(Widget widget, Widget built) {
  assert(() {
    if (built == null) {
272 273 274 275 276 277 278 279 280
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('A build function returned null.'),
        DiagnosticsProperty<Widget>('The offending widget is', widget, style: DiagnosticsTreeStyle.errorProperty),
        ErrorDescription('Build functions must never return null.'),
        ErrorHint(
          'To return an empty space that causes the building widget to fill available room, return "Container()". '
          'To return an empty space that takes as little room as possible, return "Container(width: 0.0, height: 0.0)".'
        ),
      ]);
281
    }
282
    if (widget == built) {
283 284 285 286 287 288 289 290
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('A build function returned context.widget.'),
        DiagnosticsProperty<Widget>('The offending widget is', widget, style: DiagnosticsTreeStyle.errorProperty),
        ErrorDescription(
          'Build functions must never return their BuildContext parameter\'s widget or a child that contains "context.widget". '
          'Doing so introduces a loop in the widget tree that can cause the app to crash.'
        ),
      ]);
291
    }
292
    return true;
293
  }());
294
}
295 296 297 298 299 300

/// Returns true if none of the widget library debug variables have been changed.
///
/// This function is used by the test framework to ensure that debug variables
/// haven't been inadvertently changed.
///
301
/// See [the widgets library](widgets/widgets-library.html) for a complete list.
302 303 304 305 306 307
bool debugAssertAllWidgetVarsUnset(String reason) {
  assert(() {
    if (debugPrintRebuildDirtyWidgets ||
        debugPrintBuildScope ||
        debugPrintScheduleBuildForStacks ||
        debugPrintGlobalKeyedWidgetLifecycle ||
308 309
        debugProfileBuildsEnabled ||
        debugHighlightDeprecatedWidgets) {
310
      throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('$reason')]);
311 312
    }
    return true;
313
  }());
314 315
  return true;
}