Skip to content

Commit e435b1a

Browse files
authored
Add debugPrintKeyboardEvents flag (#125629)
## Description This PR adds a new debug flag named `debugPrintKeyboardEvents` to help debugging keyboard issues. Keyboard code has some useful asserts but sometimes an assertion failure is related to the handling of previous key events. This debug flag will help understanding the flow of key events which leads to an assertion failure. ## Related Issue Fixes flutter/flutter#125627 ## Tests Adds 1 test.
1 parent 794c2e0 commit e435b1a

3 files changed

Lines changed: 79 additions & 3 deletions

File tree

packages/flutter/lib/src/services/debug.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ KeyDataTransitMode? debugKeyEventSimulatorTransitModeOverride;
2727
/// Flutter to the host platform, "down" is the host platform to flutter.
2828
bool debugProfilePlatformChannels = false;
2929

30+
/// Setting to true will cause extensive logging to occur when key events are
31+
/// received.
32+
///
33+
/// Can be used to debug keyboard issues: each time a key event is received on
34+
/// the framework side, the event details and the current pressed state will
35+
/// be printed.
36+
bool debugPrintKeyboardEvents = false;
37+
3038
/// Returns true if none of the widget library debug variables have been changed.
3139
///
3240
/// This function is used by the test framework to ensure that debug variables
@@ -38,7 +46,7 @@ bool debugAssertAllServicesVarsUnset(String reason) {
3846
if (debugKeyEventSimulatorTransitModeOverride != null) {
3947
throw FlutterError(reason);
4048
}
41-
if (debugProfilePlatformChannels) {
49+
if (debugProfilePlatformChannels || debugPrintKeyboardEvents) {
4250
throw FlutterError(reason);
4351
}
4452
return true;

packages/flutter/lib/src/services/hardware_keyboard.dart

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:ui' as ui;
77
import 'package:flutter/foundation.dart';
88

99
import 'binding.dart';
10+
import 'debug.dart';
1011
import 'raw_keyboard.dart';
1112
import 'system_channels.dart';
1213

@@ -17,6 +18,41 @@ export 'package:flutter/foundation.dart' show DiagnosticPropertiesBuilder;
1718
export 'keyboard_key.g.dart' show LogicalKeyboardKey, PhysicalKeyboardKey;
1819
export 'raw_keyboard.dart' show RawKeyEvent, RawKeyboard;
1920

21+
// When using _keyboardDebug, always call it like so:
22+
//
23+
// assert(_keyboardDebug(() => 'Blah $foo'));
24+
//
25+
// It needs to be inside the assert in order to be removed in release mode, and
26+
// it needs to use a closure to generate the string in order to avoid string
27+
// interpolation when debugPrintKeyboardEvents is false.
28+
//
29+
// It will throw a StateError if you try to call it when the app is in release
30+
// mode.
31+
bool _keyboardDebug(
32+
String Function() messageFunc, [
33+
Iterable<Object> Function()? detailsFunc,
34+
]) {
35+
if (kReleaseMode) {
36+
throw StateError(
37+
'_keyboardDebug was called in Release mode, which means they are called '
38+
'without being wrapped in an assert. Always call _keyboardDebug like so:\n'
39+
r" assert(_keyboardDebug(() => 'Blah $foo'));"
40+
);
41+
}
42+
if (!debugPrintKeyboardEvents) {
43+
return true;
44+
}
45+
debugPrint('KEYBOARD: ${messageFunc()}');
46+
final Iterable<Object> details = detailsFunc?.call() ?? const <Object>[];
47+
if (details.isNotEmpty) {
48+
for (final Object detail in details) {
49+
debugPrint(' $detail');
50+
}
51+
}
52+
// Return true so that it can be used inside of an assert.
53+
return true;
54+
}
55+
2056
/// Represents a lock mode of a keyboard, such as [KeyboardLockMode.capsLock].
2157
///
2258
/// A lock mode locks some of a keyboard's keys into a distinct mode of operation,
@@ -546,9 +582,22 @@ class HardwareKeyboard {
546582
return handled;
547583
}
548584

585+
List<String> _debugPressedKeysDetails() {
586+
if (_pressedKeys.isEmpty) {
587+
return <String>['Empty'];
588+
}
589+
final List<String> details = <String>[];
590+
for (final PhysicalKeyboardKey physicalKey in _pressedKeys.keys) {
591+
details.add('$physicalKey: ${_pressedKeys[physicalKey]}');
592+
}
593+
return details;
594+
}
595+
549596
/// Process a new [KeyEvent] by recording the state changes and dispatching
550597
/// to handlers.
551598
bool handleKeyEvent(KeyEvent event) {
599+
assert(_keyboardDebug(() => 'Key event received: $event'));
600+
assert(_keyboardDebug(() => 'Pressed state before processing the event:', _debugPressedKeysDetails));
552601
_assertEventIsRegular(event);
553602
final PhysicalKeyboardKey physicalKey = event.physicalKey;
554603
final LogicalKeyboardKey logicalKey = event.logicalKey;
@@ -568,6 +617,7 @@ class HardwareKeyboard {
568617
// Empty
569618
}
570619

620+
assert(_keyboardDebug(() => 'Pressed state after processing the event:', _debugPressedKeysDetails));
571621
return _dispatchKeyEvent(event);
572622
}
573623

packages/flutter/test/services/hardware_keyboard_test.dart

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,9 +465,27 @@ void main() {
465465
// trigger assertions.
466466
expect(record, isNull);
467467
}, variant: KeySimulatorTransitModeVariant.all());
468-
}
469-
470468

469+
testWidgets('debugPrintKeyboardEvents causes logging of key events', (WidgetTester tester) async {
470+
final bool oldDebugPrintKeyboardEvents = debugPrintKeyboardEvents;
471+
final DebugPrintCallback oldDebugPrint = debugPrint;
472+
final StringBuffer messages = StringBuffer();
473+
debugPrint = (String? message, {int? wrapWidth}) {
474+
messages.writeln(message ?? '');
475+
};
476+
debugPrintKeyboardEvents = true;
477+
try {
478+
await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
479+
} finally {
480+
debugPrintKeyboardEvents = oldDebugPrintKeyboardEvents;
481+
debugPrint = oldDebugPrint;
482+
}
483+
final String messagesStr = messages.toString();
484+
expect(messagesStr, contains('KEYBOARD: Key event received: '));
485+
expect(messagesStr, contains('KEYBOARD: Pressed state before processing the event:'));
486+
expect(messagesStr, contains('KEYBOARD: Pressed state after processing the event:'));
487+
});
488+
}
471489

472490
Future<void> _runWhileOverridingOnError(AsyncCallback body, {required FlutterExceptionHandler onError}) async {
473491
final FlutterExceptionHandler? oldFlutterErrorOnError = FlutterError.onError;

0 commit comments

Comments
 (0)