debugFillProperties method

  1. @override
void debugFillProperties(
  1. DiagnosticPropertiesBuilder properties
)
override

Add additional properties associated with the node.

Use the most specific DiagnosticsProperty existing subclass to describe each property instead of the DiagnosticsProperty base class. There are only a small number of DiagnosticsProperty subclasses each covering a common use case. Consider what values a property is relevant for users debugging as users debugging large trees are overloaded with information. Common named parameters in DiagnosticsNode subclasses help filter when and how properties are displayed.

defaultValue, showName, showSeparator, and level keep string representations of diagnostics terse and hide properties when they are not very useful.

  • Use defaultValue any time the default value of a property is uninteresting. For example, specify a default value of null any time a property being null does not indicate an error.
  • Avoid specifying the level parameter unless the result you want cannot be achieved by using the defaultValue parameter or using the ObjectFlagProperty class to conditionally display the property as a flag.
  • Specify showName and showSeparator in rare cases where the string output would look clumsy if they were not set.
    DiagnosticsProperty<Object>('child(3, 4)', null, ifNull: 'is null', showSeparator: false).toString()
    
    Shows using showSeparator to get output child(3, 4) is null which is more polished than child(3, 4): is null.
    DiagnosticsProperty<IconData>('icon', icon, ifNull: '<empty>', showName: false).toString()
    
    Shows using showName to omit the property name as in this context the property name does not add useful information.

ifNull, ifEmpty, unit, and tooltip make property descriptions clearer. The examples in the code sample below illustrate good uses of all of these parameters.

DiagnosticsProperty subclasses for primitive types

  • StringProperty, which supports automatically enclosing a String value in quotes.
  • DoubleProperty, which supports specifying a unit of measurement for a double value.
  • PercentProperty, which clamps a double to between 0 and 1 and formats it as a percentage.
  • IntProperty, which supports specifying a unit of measurement for an int value.
  • FlagProperty, which formats a bool value as one or more flags. Depending on the use case it is better to format a bool as DiagnosticsProperty<bool> instead of using FlagProperty as the output is more verbose but unambiguous.

Other important DiagnosticsProperty variants

  • EnumProperty, which provides terse descriptions of enum values working around limitations of the toString implementation for Dart enum types.
  • IterableProperty, which handles iterable values with display customizable depending on the DiagnosticsTreeStyle used.
  • ObjectFlagProperty, which provides terse descriptions of whether a property value is present or not. For example, whether an onClick callback is specified or an animation is in progress.
  • ColorProperty, which must be used if the property value is a Color or one of its subclasses.
  • IconDataProperty, which must be used if the property value is of type IconData.

If none of these subclasses apply, use the DiagnosticsProperty constructor or in rare cases create your own DiagnosticsProperty subclass as in the case for TransformProperty which handles Matrix4 that represent transforms. Generally any property value with a good toString method implementation works fine using DiagnosticsProperty directly.

This example shows best practices for implementing debugFillProperties illustrating use of all common DiagnosticsProperty subclasses and all common DiagnosticsProperty parameters.
link
class ExampleObject extends ExampleSuperclass {

  // ...various members and properties...

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    // Always add properties from the base class first.
    super.debugFillProperties(properties);

    // Omit the property name 'message' when displaying this String property
    // as it would just add visual noise.
    properties.add(StringProperty('message', message, showName: false));

    properties.add(DoubleProperty('stepWidth', stepWidth));

    // A scale of 1.0 does nothing so should be hidden.
    properties.add(DoubleProperty('scale', scale, defaultValue: 1.0));

    // If the hitTestExtent matches the paintExtent, it is just set to its
    // default value so is not relevant.
    properties.add(DoubleProperty('hitTestExtent', hitTestExtent, defaultValue: paintExtent));

    // maxWidth of double.infinity indicates the width is unconstrained and
    // so maxWidth has no impact.
    properties.add(DoubleProperty('maxWidth', maxWidth, defaultValue: double.infinity));

    // Progress is a value between 0 and 1 or null. Showing it as a
    // percentage makes the meaning clear enough that the name can be
    // hidden.
    properties.add(PercentProperty(
      'progress',
      progress,
      showName: false,
      ifNull: '<indeterminate>',
    ));

    // Most text fields have maxLines set to 1.
    properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));

    // Specify the unit as otherwise it would be unclear that time is in
    // milliseconds.
    properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms'));

    // Tooltip is used instead of unit for this case as a unit should be a
    // terse description appropriate to display directly after a number
    // without a space.
    properties.add(DoubleProperty(
      'device pixel ratio',
      devicePixelRatio,
      tooltip: 'physical pixels per logical pixel',
    ));

    // Displaying the depth value would be distracting. Instead only display
    // if the depth value is missing.
    properties.add(ObjectFlagProperty<int>('depth', depth, ifNull: 'no depth'));

    // bool flag that is only shown when the value is true.
    properties.add(FlagProperty('using primary controller', value: primary));

    properties.add(FlagProperty(
      'isCurrent',
      value: isCurrent,
      ifTrue: 'active',
      ifFalse: 'inactive',
    ));

    properties.add(DiagnosticsProperty<bool>('keepAlive', keepAlive));

    // FlagProperty could have also been used in this case.
    // This option results in the text "obscureText: true" instead
    // of "obscureText" which is a bit more verbose but a bit clearer.
    properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));

    properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
    properties.add(EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat));

    // Warn users when the widget is missing but do not show the value.
    properties.add(ObjectFlagProperty<Widget>('widget', widget, ifNull: 'no widget'));

    properties.add(IterableProperty<BoxShadow>(
      'boxShadow',
      boxShadow,
      defaultValue: null,
      style: style,
    ));

    // Getting the value of size throws an exception unless hasSize is true.
    properties.add(DiagnosticsProperty<Size>.lazy(
      'size',
      () => size,
      description: '${ hasSize ? size : "MISSING" }',
    ));

    // If the `toString` method for the property value does not provide a
    // good terse description, write a DiagnosticsProperty subclass as in
    // the case of TransformProperty which displays a nice debugging view
    // of a Matrix4 that represents a transform.
    properties.add(TransformProperty('transform', transform));

    // If the value class has a good `toString` method, use
    // DiagnosticsProperty<YourValueType>. Specifying the value type ensures
    // that debugging tools always know the type of the field and so can
    // provide the right UI affordances. For example, in this case even
    // if color is null, a debugging tool still knows the value is a Color
    // and can display relevant color related UI.
    properties.add(DiagnosticsProperty<Color>('color', color));

    // Use a custom description to generate a more terse summary than the
    // `toString` method on the map class.
    properties.add(DiagnosticsProperty<Map<Listenable, VoidCallback>>(
      'handles',
      handles,
      description: handles != null
        ? '${handles!.length} active client${ handles!.length == 1 ? "" : "s" }'
        : null,
      ifNull: 'no notifications ever received',
      showName: false,
    ));
  }
}

Used by toDiagnosticsNode and toString.

Do not add values that have lifetime shorter than the object.

Implementation

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  super.debugFillProperties(properties);
  bool hideOwner = true;
  if (_dirty) {
    final bool inDirtyNodes = owner != null && owner!._dirtyNodes.contains(this);
    properties.add(FlagProperty('inDirtyNodes', value: inDirtyNodes, ifTrue: 'dirty', ifFalse: 'STALE'));
    hideOwner = inDirtyNodes;
  }
  properties.add(DiagnosticsProperty<SemanticsOwner>('owner', owner, level: hideOwner ? DiagnosticLevel.hidden : DiagnosticLevel.info));
  properties.add(FlagProperty('isMergedIntoParent', value: isMergedIntoParent, ifTrue: 'merged up ⬆️'));
  properties.add(FlagProperty('mergeAllDescendantsIntoThisNode', value: mergeAllDescendantsIntoThisNode, ifTrue: 'merge boundary ⛔️'));
  final Offset? offset = transform != null ? MatrixUtils.getAsTranslation(transform!) : null;
  if (offset != null) {
    properties.add(DiagnosticsProperty<Rect>('rect', rect.shift(offset), showName: false));
  } else {
    final double? scale = transform != null ? MatrixUtils.getAsScale(transform!) : null;
    String? description;
    if (scale != null) {
      description = '$rect scaled by ${scale.toStringAsFixed(1)}x';
    } else if (transform != null && !MatrixUtils.isIdentity(transform!)) {
      final String matrix = transform.toString().split('\n').take(4).map<String>((String line) => line.substring(4)).join('; ');
      description = '$rect with transform [$matrix]';
    }
    properties.add(DiagnosticsProperty<Rect>('rect', rect, description: description, showName: false));
  }
  properties.add(IterableProperty<String>('tags', tags?.map((SemanticsTag tag) => tag.name), defaultValue: null));
  final List<String> actions = _actions.keys.map<String>((SemanticsAction action) => '${action.name}${_debugIsActionBlocked(action) ? '🚫️' : ''}').toList()..sort();
  final List<String?> customSemanticsActions = _customSemanticsActions.keys
    .map<String?>((CustomSemanticsAction action) => action.label)
    .toList();
  properties.add(IterableProperty<String>('actions', actions, ifEmpty: null));
  properties.add(IterableProperty<String?>('customActions', customSemanticsActions, ifEmpty: null));
  final List<String> flags = SemanticsFlag.values.where((SemanticsFlag flag) => hasFlag(flag)).map((SemanticsFlag flag) => flag.name).toList();
  properties.add(IterableProperty<String>('flags', flags, ifEmpty: null));
  properties.add(FlagProperty('isInvisible', value: isInvisible, ifTrue: 'invisible'));
  properties.add(FlagProperty('isHidden', value: hasFlag(SemanticsFlag.isHidden), ifTrue: 'HIDDEN'));
  properties.add(StringProperty('identifier', _identifier, defaultValue: ''));
  properties.add(AttributedStringProperty('label', _attributedLabel));
  properties.add(AttributedStringProperty('value', _attributedValue));
  properties.add(AttributedStringProperty('increasedValue', _attributedIncreasedValue));
  properties.add(AttributedStringProperty('decreasedValue', _attributedDecreasedValue));
  properties.add(AttributedStringProperty('hint', _attributedHint));
  properties.add(StringProperty('tooltip', _tooltip, defaultValue: ''));
  properties.add(EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null));
  properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
  if (_textSelection?.isValid ?? false) {
    properties.add(MessageProperty('text selection', '[${_textSelection!.start}, ${_textSelection!.end}]'));
  }
  properties.add(IntProperty('platformViewId', platformViewId, defaultValue: null));
  properties.add(IntProperty('maxValueLength', maxValueLength, defaultValue: null));
  properties.add(IntProperty('currentValueLength', currentValueLength, defaultValue: null));
  properties.add(IntProperty('scrollChildren', scrollChildCount, defaultValue: null));
  properties.add(IntProperty('scrollIndex', scrollIndex, defaultValue: null));
  properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
  properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null));
  properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null));
  properties.add(IntProperty('indexInParent', indexInParent, defaultValue: null));
  properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0));
  properties.add(DoubleProperty('thickness', thickness, defaultValue: 0.0));
  properties.add(IntProperty('headingLevel', _headingLevel, defaultValue: 0));
}