Make `_focusDebug` not interpolate in debug mode (#119680)
* Make _focusDebug not interpolate in debug mode
* Add test
* Revert undesired change
* Fix test to fail before too
* Remove accidental skips
* Switch to using a generating closure for arguments.
* Remove a word
diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart
index b915c88..de94e41 100644
--- a/packages/flutter/lib/src/widgets/focus_manager.dart
+++ b/packages/flutter/lib/src/widgets/focus_manager.dart
@@ -22,16 +22,38 @@
/// be logged.
bool debugFocusChanges = false;
-bool _focusDebug(String message, [Iterable<String>? details]) {
- if (debugFocusChanges) {
- debugPrint('FOCUS: $message');
- if (details != null && details.isNotEmpty) {
- for (final String detail in details) {
- debugPrint(' $detail');
- }
+// When using _focusDebug, always call it like so:
+//
+// assert(_focusDebug(() => 'Blah $foo'));
+//
+// It needs to be inside the assert in order to be removed in release mode, and
+// it needs to use a closure to generate the string in order to avoid string
+// interpolation when debugFocusChanges is false.
+//
+// It will throw a StateError if you try to call it when the app is in release
+// mode.
+bool _focusDebug(
+ String Function() messageFunc, [
+ Iterable<Object> Function()? detailsFunc,
+]) {
+ if (kReleaseMode) {
+ throw StateError(
+ '_focusDebug was called in Release mode. It should always be wrapped in '
+ 'an assert. Always call _focusDebug like so:\n'
+ r" assert(_focusDebug(() => 'Blah $foo'));"
+ );
+ }
+ if (!debugFocusChanges) {
+ return true;
+ }
+ debugPrint('FOCUS: ${messageFunc()}');
+ final Iterable<Object> details = detailsFunc?.call() ?? const <Object>[];
+ if (details.isNotEmpty) {
+ for (final Object detail in details) {
+ debugPrint(' $detail');
}
}
- // Return true so that it can be easily used inside of an assert.
+ // Return true so that it can be used inside of an assert.
return true;
}
@@ -118,10 +140,10 @@
&& scope.focusedChild == null
&& autofocusNode.ancestors.contains(scope);
if (shouldApply) {
- assert(_focusDebug('Applying autofocus: $autofocusNode'));
+ assert(_focusDebug(() => 'Applying autofocus: $autofocusNode'));
autofocusNode._doRequestFocus(findFirstFocus: true);
} else {
- assert(_focusDebug('Autofocus request discarded for node: $autofocusNode.'));
+ assert(_focusDebug(() => 'Autofocus request discarded for node: $autofocusNode.'));
}
}
}
@@ -169,7 +191,7 @@
///
/// Calling [FocusNode.dispose] will also automatically detach the node.
void detach() {
- assert(_focusDebug('Detaching node:', <String>[_node.toString(), 'With enclosing scope ${_node.enclosingScope}']));
+ assert(_focusDebug(() => 'Detaching node:', () => <Object>[_node, 'With enclosing scope ${_node.enclosingScope}']));
if (isAttached) {
if (_node.hasPrimaryFocus || (_node._manager != null && _node._manager!._markedForFocus == _node)) {
_node.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
@@ -877,7 +899,7 @@
scope._doRequestFocus(findFirstFocus: true);
break;
}
- assert(_focusDebug('Unfocused node:', <String>['primary focus was $this', 'next focus will be ${_manager?._markedForFocus}']));
+ assert(_focusDebug(() => 'Unfocused node:', () => <Object>['primary focus was $this', 'next focus will be ${_manager?._markedForFocus}']));
}
/// Removes the keyboard token from this focus node if it has one.
@@ -1063,7 +1085,7 @@
// Note that this is overridden in FocusScopeNode.
void _doRequestFocus({required bool findFirstFocus}) {
if (!canRequestFocus) {
- assert(_focusDebug('Node NOT requesting focus because canRequestFocus is false: $this'));
+ assert(_focusDebug(() => 'Node NOT requesting focus because canRequestFocus is false: $this'));
return;
}
// If the node isn't part of the tree, then we just defer the focus request
@@ -1078,7 +1100,7 @@
return;
}
_hasKeyboardToken = true;
- assert(_focusDebug('Node requesting focus: $this'));
+ assert(_focusDebug(() => 'Node requesting focus: $this'));
_markNextFocus(this);
}
@@ -1109,7 +1131,7 @@
FocusNode scopeFocus = this;
for (final FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) {
assert(scopeFocus != ancestor, 'Somehow made a loop by setting focusedChild to its scope.');
- assert(_focusDebug('Setting $scopeFocus as focused child for scope:', <String>[ancestor.toString()]));
+ assert(_focusDebug(() => 'Setting $scopeFocus as focused child for scope:', () => <Object>[ancestor]));
// Remove it anywhere in the focused child history.
ancestor._focusedChildren.remove(scopeFocus);
// Add it to the end of the list, which is also the top of the queue: The
@@ -1276,7 +1298,7 @@
/// tree, the given scope must be a descendant of this scope.
void setFirstFocus(FocusScopeNode scope) {
assert(scope != this, 'Unexpected self-reference in setFirstFocus.');
- assert(_focusDebug('Setting scope as first focus in $this to node:', <String>[scope.toString()]));
+ assert(_focusDebug(() => 'Setting scope as first focus in $this to node:', () => <Object>[scope]));
if (scope._parent == null) {
_reparent(scope);
}
@@ -1306,7 +1328,7 @@
}
assert(_manager != null);
- assert(_focusDebug('Autofocus scheduled for $node: scope $this'));
+ assert(_focusDebug(() => 'Autofocus scheduled for $node: scope $this'));
_manager?._pendingAutofocuses.add(_Autofocus(scope: this, autofocusNode: node));
_manager?._markNeedsUpdate();
}
@@ -1542,7 +1564,7 @@
void _markDetached(FocusNode node) {
// The node has been removed from the tree, so it no longer needs to be
// notified of changes.
- assert(_focusDebug('Node was detached: $node'));
+ assert(_focusDebug(() => 'Node was detached: $node'));
if (_primaryFocus == node) {
_primaryFocus = null;
}
@@ -1551,7 +1573,7 @@
void _markPropertiesChanged(FocusNode node) {
_markNeedsUpdate();
- assert(_focusDebug('Properties changed for node $node.'));
+ assert(_focusDebug(() => 'Properties changed for node $node.'));
_dirtyNodes.add(node);
}
@@ -1575,7 +1597,7 @@
// Request that an update be scheduled, optionally requesting focus for the
// given newFocus node.
void _markNeedsUpdate() {
- assert(_focusDebug('Scheduling update, current focus is $_primaryFocus, next focus will be $_markedForFocus'));
+ assert(_focusDebug(() => 'Scheduling update, current focus is $_primaryFocus, next focus will be $_markedForFocus'));
if (_haveScheduledUpdate) {
return;
}
@@ -1597,7 +1619,7 @@
// then revert to the root scope.
_markedForFocus = rootScope;
}
- assert(_focusDebug('Refreshing focus state. Next focus will be $_markedForFocus'));
+ assert(_focusDebug(() => 'Refreshing focus state. Next focus will be $_markedForFocus'));
// A node has requested to be the next focus, and isn't already the primary
// focus.
if (_markedForFocus != null && _markedForFocus != _primaryFocus) {
@@ -1613,7 +1635,7 @@
}
assert(_markedForFocus == null);
if (previousFocus != _primaryFocus) {
- assert(_focusDebug('Updating focus from $previousFocus to $_primaryFocus'));
+ assert(_focusDebug(() => 'Updating focus from $previousFocus to $_primaryFocus'));
if (previousFocus != null) {
_dirtyNodes.add(previousFocus);
}
@@ -1624,7 +1646,7 @@
for (final FocusNode node in _dirtyNodes) {
node._notify();
}
- assert(_focusDebug('Notified ${_dirtyNodes.length} dirty nodes:', _dirtyNodes.toList().map<String>((FocusNode node) => node.toString())));
+ assert(_focusDebug(() => 'Notified ${_dirtyNodes.length} dirty nodes:', () => _dirtyNodes));
_dirtyNodes.clear();
if (previousFocus != _primaryFocus) {
notifyListeners();
@@ -1766,9 +1788,9 @@
_lastInteractionWasTouch = false;
updateMode();
- assert(_focusDebug('Received key event $message'));
+ assert(_focusDebug(() => 'Received key event $message'));
if (FocusManager.instance.primaryFocus == null) {
- assert(_focusDebug('No primary focus for key event, ignored: $message'));
+ assert(_focusDebug(() => 'No primary focus for key event, ignored: $message'));
return false;
}
@@ -1794,11 +1816,11 @@
case KeyEventResult.ignored:
continue;
case KeyEventResult.handled:
- assert(_focusDebug('Node $node handled key event $message.'));
+ assert(_focusDebug(() => 'Node $node handled key event $message.'));
handled = true;
break;
case KeyEventResult.skipRemainingHandlers:
- assert(_focusDebug('Node $node stopped key event propagation: $message.'));
+ assert(_focusDebug(() => 'Node $node stopped key event propagation: $message.'));
handled = false;
break;
}
@@ -1808,7 +1830,7 @@
break;
}
if (!handled) {
- assert(_focusDebug('Key event not handled by anyone: $message.'));
+ assert(_focusDebug(() => 'Key event not handled by anyone: $message.'));
}
return handled;
}
diff --git a/packages/flutter/test/widgets/focus_manager_test.dart b/packages/flutter/test/widgets/focus_manager_test.dart
index 53501fc..21c8b0d 100644
--- a/packages/flutter/test/widgets/focus_manager_test.dart
+++ b/packages/flutter/test/widgets/focus_manager_test.dart
@@ -1700,4 +1700,61 @@
expect(messagesStr, contains('FOCUS: Notified 2 dirty nodes'));
expect(messagesStr, contains(RegExp(r'FOCUS: Scheduling update, current focus is null, next focus will be FocusScopeNode#.*parent1')));
});
+
+ testWidgets("doesn't call toString on a focus node when debugFocusChanges is false", (WidgetTester tester) async {
+ final bool oldDebugFocusChanges = debugFocusChanges;
+ final DebugPrintCallback oldDebugPrint = debugPrint;
+ final StringBuffer messages = StringBuffer();
+ debugPrint = (String? message, {int? wrapWidth}) {
+ messages.writeln(message ?? '');
+ };
+ Future<void> testDebugFocusChanges() async {
+ final BuildContext context = await setupWidget(tester);
+ final FocusScopeNode parent1 = FocusScopeNode(debugLabel: 'parent1');
+ final FocusAttachment parent1Attachment = parent1.attach(context);
+ final FocusNode child1 = debugFocusChanges ? FocusNode(debugLabel: 'child1') : _LoggingTestFocusNode(debugLabel: 'child1');
+ final FocusAttachment child1Attachment = child1.attach(context);
+ parent1Attachment.reparent(parent: tester.binding.focusManager.rootScope);
+ child1Attachment.reparent(parent: parent1);
+
+ child1.requestFocus();
+ await tester.pump();
+ child1.dispose();
+ parent1.dispose();
+ await tester.pump();
+ }
+ try {
+ debugFocusChanges = false;
+ await testDebugFocusChanges();
+ expect(messages, isEmpty);
+ expect(tester.takeException(), isNull);
+ debugFocusChanges = true;
+ await testDebugFocusChanges();
+ expect(messages.toString(), contains('FOCUS: Notified 3 dirty nodes:'));
+ expect(tester.takeException(), isNull);
+ } finally {
+ debugFocusChanges = oldDebugFocusChanges;
+ debugPrint = oldDebugPrint;
+ }
+ });
+}
+
+class _LoggingTestFocusNode extends FocusNode {
+ _LoggingTestFocusNode({super.debugLabel});
+
+ @override
+ String toString({
+ DiagnosticLevel minLevel = DiagnosticLevel.debug,
+ }) {
+ throw StateError("Shouldn't call toString here");
+ }
+
+ @override
+ String toStringDeep({
+ String prefixLineOne = '',
+ String? prefixOtherLines,
+ DiagnosticLevel minLevel = DiagnosticLevel.debug,
+ }) {
+ throw StateError("Shouldn't call toStringDeep here");
+ }
}