Disable hardware keyboard regularity warning by default (#181894)
Suppresses https://github.com/flutter/flutter/issues/125975
This PR disable hardware keyboard regularity warning unless
`debugPrintKeyboardEvents` is set.
Keyboard state synchronization has proven extremely difficult, largely
due to the asynchronous communication between the engine and framework.
Unfortunately, I haven't had the bandwidth to fix it, causing developers
to experience false errors during hot restarts. It's time to acknowledge
this and prioritize development experience over idealism by disabling
these useless warnings.
I apologize for the issues I've caused over the years. Perhaps someday
we can make this work with a fully regular event stream.
Replaces https://github.com/flutter/flutter/pull/181719 .
## Pre-launch Checklist
- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/packages/flutter/lib/src/services/hardware_keyboard.dart b/packages/flutter/lib/src/services/hardware_keyboard.dart
index 90f7000..9fae9c8 100644
--- a/packages/flutter/lib/src/services/hardware_keyboard.dart
+++ b/packages/flutter/lib/src/services/hardware_keyboard.dart
@@ -504,33 +504,42 @@
isLogicalKeyPressed(LogicalKeyboardKey.metaRight);
}
- void _assertEventIsRegular(KeyEvent event) {
+ // Print debug messages if the event is inconsistent
+ // with the current state, and if [debugPrintKeyboardEvents] is true.
+ void _logEventIfIrregular(KeyEvent event) {
assert(() {
const common =
- 'If this occurs in real application, please report this '
- 'bug to Flutter. If this occurs in unit tests, please ensure that '
- "simulated events follow Flutter's event model as documented in "
- '`HardwareKeyboard`. This was the event: ';
+ 'This is typically either due to https://github.com/flutter/flutter/issues/125975, '
+ "or a bug in the embedding's key event conciliation logic.";
if (event is KeyDownEvent) {
- assert(
- !_pressedKeys.containsKey(event.physicalKey),
- 'A ${event.runtimeType} is dispatched, but the state shows that the physical '
- 'key is already pressed. $common$event',
- );
+ if (_pressedKeys.containsKey(event.physicalKey)) {
+ _keyboardDebug(
+ () =>
+ 'ERROR: Received unexpected ${event.runtimeType} for key that is already pressed.\n'
+ '$common\n'
+ ' Event: $event\n'
+ ' Pressed logical key: ${_pressedKeys[event.physicalKey]}',
+ );
+ }
} else if (event is KeyRepeatEvent || event is KeyUpEvent) {
- assert(
- _pressedKeys.containsKey(event.physicalKey),
- 'A ${event.runtimeType} is dispatched, but the state shows that the physical '
- 'key is not pressed. $common$event',
- );
- assert(
- _pressedKeys[event.physicalKey] == event.logicalKey,
- 'A ${event.runtimeType} is dispatched, but the state shows that the physical '
- 'key is pressed on a different logical key. $common$event '
- 'and the recorded logical key ${_pressedKeys[event.physicalKey]}',
- );
+ if (!_pressedKeys.containsKey(event.physicalKey)) {
+ _keyboardDebug(
+ () =>
+ 'ERROR: Received unexpected ${event.runtimeType} for key that is not pressed:\n'
+ '$common\n'
+ ' Event: $event',
+ );
+ } else if (_pressedKeys[event.physicalKey] != event.logicalKey) {
+ _keyboardDebug(
+ () =>
+ 'ERROR: Received unexpected ${event.runtimeType} for key with mismatched logical key:\n'
+ '$common\n'
+ ' Event: $event\n'
+ ' Pressed logical key: ${_pressedKeys[event.physicalKey]}',
+ );
+ }
} else {
- assert(false, 'Unexpected key event class ${event.runtimeType}');
+ assert(false, 'Received unexpected key event class ${event.runtimeType}');
}
return true;
}());
@@ -652,12 +661,14 @@
/// Process a new [KeyEvent] by recording the state changes and dispatching
/// to handlers.
+ ///
+ /// Returns true if any handler handled the event.
bool handleKeyEvent(KeyEvent event) {
assert(_keyboardDebug(() => 'Key event received: $event'));
assert(
_keyboardDebug(() => 'Pressed state before processing the event:', _debugPressedKeysDetails),
);
- _assertEventIsRegular(event);
+ _logEventIfIrregular(event);
final PhysicalKeyboardKey physicalKey = event.physicalKey;
final LogicalKeyboardKey logicalKey = event.logicalKey;
if (event is KeyDownEvent) {
@@ -673,7 +684,8 @@
} else if (event is KeyUpEvent) {
_pressedKeys.remove(physicalKey);
} else if (event is KeyRepeatEvent) {
- // Empty
+ // Update the logical key in case it has changed.
+ _pressedKeys[physicalKey] = logicalKey;
}
assert(
diff --git a/packages/flutter/test/services/hardware_keyboard_test.dart b/packages/flutter/test/services/hardware_keyboard_test.dart
index ec69b41..772e0ce 100644
--- a/packages/flutter/test/services/hardware_keyboard_test.dart
+++ b/packages/flutter/test/services/hardware_keyboard_test.dart
@@ -9,6 +9,11 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
+void expectState(Map<PhysicalKeyboardKey, LogicalKeyboardKey> expectedKeys) {
+ expect(HardwareKeyboard.instance.physicalKeysPressed, equals(expectedKeys.keys.toSet()));
+ expect(HardwareKeyboard.instance.logicalKeysPressed, equals(expectedKeys.values.toSet()));
+}
+
void main() {
testWidgets(
'HardwareKeyboard records pressed keys and enabled locks',
@@ -596,6 +601,82 @@
expect(messagesStr, contains('KEYBOARD: Pressed state before processing the event:'));
expect(messagesStr, contains('KEYBOARD: Pressed state after processing the event:'));
});
+
+ testWidgets(
+ 'Irregular key events are processed',
+ (WidgetTester tester) async {
+ final logs = <String>[];
+
+ debugPrintKeyboardEvents = true;
+ final DebugPrintCallback oldDebugPrint = debugPrint;
+ debugPrint = (String? message, {int? wrapWidth}) {
+ if (message != null) {
+ logs.add(message);
+ }
+ };
+
+ Future<void> expectIsRegular(Future<bool> simulateEvent, bool isRegular) async {
+ // All simulated events should return a `handled` result of false to
+ // indicate that they are indeed dispatched to handlers.
+ expect(await simulateEvent, isFalse);
+
+ final String log = logs.join('\n');
+ final Matcher errorCheck = contains('ERROR');
+ expect(log, isRegular ? isNot(errorCheck) : errorCheck);
+ logs.clear();
+ }
+
+ // 1. Press keyA.
+ await expectIsRegular(simulateKeyDownEvent(LogicalKeyboardKey.keyA), true);
+ expectState(<PhysicalKeyboardKey, LogicalKeyboardKey>{
+ PhysicalKeyboardKey.keyA: LogicalKeyboardKey.keyA,
+ });
+ // Press keyA again with a mismatched logical key, which should affect the
+ // state.
+ await expectIsRegular(
+ simulateKeyDownEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA),
+ false,
+ );
+ expectState(<PhysicalKeyboardKey, LogicalKeyboardKey>{
+ PhysicalKeyboardKey.keyA: LogicalKeyboardKey.keyB,
+ });
+
+ // 2. Release keyA.
+ await expectIsRegular(
+ simulateKeyUpEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA),
+ true,
+ );
+ expectState(<PhysicalKeyboardKey, LogicalKeyboardKey>{});
+
+ // Release keyA again.
+ await expectIsRegular(simulateKeyUpEvent(LogicalKeyboardKey.keyA), false);
+ expectState(<PhysicalKeyboardKey, LogicalKeyboardKey>{});
+
+ // 3. Send a repeat event for keyA, which should affect the state.
+ await expectIsRegular(simulateKeyRepeatEvent(LogicalKeyboardKey.keyA), false);
+ expectState(<PhysicalKeyboardKey, LogicalKeyboardKey>{
+ PhysicalKeyboardKey.keyA: LogicalKeyboardKey.keyA,
+ });
+
+ // 4. Send a repeat event with a mismatched logical key, which should
+ // affect the state.
+ await expectIsRegular(
+ simulateKeyDownEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA),
+ false,
+ );
+ expectState(<PhysicalKeyboardKey, LogicalKeyboardKey>{
+ PhysicalKeyboardKey.keyA: LogicalKeyboardKey.keyB,
+ });
+
+ // 5. Send a key up event with a mismatched logical key, which should affect the state.
+ await expectIsRegular(simulateKeyUpEvent(LogicalKeyboardKey.keyA), false);
+ expectState(<PhysicalKeyboardKey, LogicalKeyboardKey>{});
+
+ debugPrintKeyboardEvents = false;
+ debugPrint = oldDebugPrint;
+ },
+ variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData(),
+ );
}
Future<void> _runWhileOverridingOnError(
diff --git a/packages/flutter_test/test/event_simulation_test.dart b/packages/flutter_test/test/event_simulation_test.dart
index 59d8361..bde0c86 100644
--- a/packages/flutter_test/test/event_simulation_test.dart
+++ b/packages/flutter_test/test/event_simulation_test.dart
@@ -46,6 +46,22 @@
}
}
+Future<void> _shouldDebugPrint(AsyncValueGetter<void> func, String containsString) async {
+ final printedMessages = <String>[];
+ final DebugPrintCallback oldDebugPrint = debugPrint;
+ debugPrint = (String? message, {int? wrapWidth}) {
+ printedMessages.add(message ?? '');
+ };
+
+ try {
+ await func();
+ } finally {
+ expect(printedMessages, isNotEmpty);
+ expect(printedMessages.join('\n'), contains(containsString));
+ debugPrint = oldDebugPrint;
+ }
+}
+
void main() {
testWidgets('default transit mode is keyDataThenRawKeyData', (WidgetTester tester) async {
expect(KeyEventSimulator.transitMode, KeyDataTransitMode.keyDataThenRawKeyData);
@@ -551,15 +567,19 @@
);
events.clear();
+ // Enable irregular key event warning.
+ debugPrintKeyboardEvents = true;
// A (physical keyA, logical keyB) is released.
//
- // Since this event is transmitted to HardwareKeyboard as-is, it will be rejected due to
- // inconsistent logical key. This does not indicate behavioral difference,
- // since KeyData is will never send malformed data sequence in real applications.
- await _shouldThrow<AssertionError>(
- () => simulateKeyUpEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA),
- );
+ // Since this event is transmitted to HardwareKeyboard as-is, it will
+ // trigger irregular key event warning due to inconsistent logical key. This
+ // does not indicate behavioral difference, since KeyData will never send
+ // malformed data sequence in real applications.
+ await _shouldDebugPrint(() {
+ return simulateKeyUpEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA);
+ }, 'Received unexpected KeyUpEvent for key with mismatched logical key:');
+ debugPrintKeyboardEvents = false;
debugKeyEventSimulatorTransitModeOverride = null;
});