In Flutter, when do you read keys with Focus.onKeyEvent versus a HardwareKeyboard.instance handler, and what replaced RawKeyEvent?
answer
- KeyDownEvent, KeyRepeatEvent, KeyUpEvent
- focus-scoped versus app-wide
- handled, ignored, skipRemainingHandlers
- logicalKey versus physicalKey
- RawKeyEvent deprecated in Flutter 3.19
basics
~10 sFocus.onKeyEvent receives KeyEvents only while focus is inside its subtree and returns a KeyEventResult that can stop bubbling. HardwareKeyboard.instance.addHandler sees every key app-wide. Both replace RawKeyEvent, RawKeyboard and FocusNode.onKey, deprecated in Flutter 3.19.
solid answer
~40 sKey input arrives as `KeyEvent` subclasses — `KeyDownEvent`, `KeyRepeatEvent`, `KeyUpEvent` — carrying `logicalKey` (what the key means under the current layout), `physicalKey` (where it is on the keyboard) and `character`. `Focus.onKeyEvent` is focus-scoped: events start at the primary focus and bubble up; returning `KeyEventResult.handled` stops them, `skipRemainingHandlers` stops Flutter's handlers but lets the platform process the key, `ignored` passes it on. With no primary focus the focus system ignores the key. `HardwareKeyboard.instance.addHandler` registers a global `bool Function(KeyEvent)` that runs before the focus tree for every key; all handlers run whatever they return, `true` only tells the engine the key was handled, and you must `removeHandler` in `dispose`. `HardwareKeyboard.instance.isControlPressed` and `logicalKeysPressed` answer modifier state. `RawKeyEvent`, `RawKeyboard`, `RawKeyboardListener` and `FocusNode.onKey` were deprecated in Flutter 3.19.
code
dart · 44 linesimport 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
/// Collects input from a keyboard-emulating barcode scanner, whatever is focused.
class ScannerListener extends StatefulWidget {
const ScannerListener({super.key, required this.onScan, required this.child});
final ValueChanged<String> onScan;
final Widget child;
@override
State<ScannerListener> createState() => _ScannerListenerState();
}
class _ScannerListenerState extends State<ScannerListener> {
final StringBuffer _buffer = StringBuffer();
bool _onKey(KeyEvent event) {
if (event is! KeyDownEvent) return false;
if (event.logicalKey == LogicalKeyboardKey.enter) {
if (_buffer.isNotEmpty) widget.onScan(_buffer.toString());
_buffer.clear();
return false;
}
final String? char = event.character;
if (char != null) _buffer.write(char);
return false; // Let the focus tree process the key as well.
}
@override
void initState() {
super.initState();
HardwareKeyboard.instance.addHandler(_onKey);
}
@override
void dispose() {
HardwareKeyboard.instance.removeHandler(_onKey);
super.dispose();
}
@override
Widget build(BuildContext context) => widget.child;
}go deeper
Recall the three event classes, KeyDownEvent, KeyRepeatEvent and KeyUpEvent, and that Focus.onKeyEvent only fires while focus is inside it.
Explain bubbling from the primary focus and the three KeyEventResult values, and when a global HardwareKeyboard handler is justified.
Migrate raw key code, pick logical versus physical keys per feature, and keep global handlers from leaking past dispose.
Decide which input belongs in the focus tree and which may be global, so device integrations never starve focus-scoped shortcuts.
## The key event model Since Flutter 3.19 deprecated the old raw key API (the annotations read "after v3.18.0-2.0.pre"), all hardware keyboard input is modelled as **`KeyEvent`** objects from `package:flutter/services.dart`: - `KeyDownEvent` — a key went down. - `KeyRepeatEvent` — the key is held and the OS is auto-repeating. - `KeyUpEvent` — the key was released. Each event carries: - `logicalKey` — a `LogicalKeyboardKey`, the **meaning** of the key under the current layout (the key that types "z" on a German layout reports `keyZ`). Use it for shortcuts and commands. - `physicalKey` — a `PhysicalKeyboardKey`, the **position** on the keyboard regardless of layout. Use it for WASD-style controls. - `character` — the text the key produced, or null for keys such as arrows and modifiers. Modifier state is not on the event; ask `HardwareKeyboard.instance`: `isControlPressed`, `isShiftPressed`, `isAltPressed`, `isMetaPressed`, or `logicalKeysPressed` for the full set. ## Focus-scoped handlers: Focus.onKeyEvent `Focus(onKeyEvent: (FocusNode node, KeyEvent event) => ...)` is the normal way for a widget to react to keys. Dispatch works like this: 1. If no node has primary focus, the focus system ignores the event. 2. Early handlers registered with `FocusManager.instance.addEarlyKeyEventHandler` run first. 3. The event goes to `primaryFocus.onKeyEvent`, then to each ancestor's, until one returns something other than `ignored`. 4. Late handlers registered with `addLateKeyEventHandler` run if nothing handled it. | `KeyEventResult` | Flutter's remaining handlers | Platform | |---|---|---| | `handled` | stop | told the key was handled | | `skipRemainingHandlers` | stop | may still process it (for example, as text input) | | `ignored` | continue bubbling | may process it if nobody handles it | `Shortcuts` and `CallbackShortcuts` are built on exactly this mechanism, which is why they need focus inside them. `KeyboardListener` is a convenience widget that takes a required `focusNode` and an optional `onKeyEvent` callback; it only listens and cannot mark a key handled. ## App-wide handlers: HardwareKeyboard `HardwareKeyboard.instance.addHandler(handler)` registers a `KeyEventCallback`, a `bool Function(KeyEvent)`, that receives **every** key event regardless of focus. Three rules from the source: - Handlers run **before** the event is dispatched to the focus tree. - **All** registered handlers run in order whatever they return, and the focus tree still receives the event afterwards. Returning `true` only reports to the engine that Flutter handled the key, which stops it from reaching other native components. - A handler added by an object must be removed with `removeHandler` before that object is disposed. That makes it the right tool for input that is not about focus: a USB barcode scanner on a point-of-sale terminal that "types" digits followed by Enter, a global push-to-talk key, or diagnostics logging. For anything tied to a region of the UI, prefer `Focus.onKeyEvent` or `Shortcuts`, which respect focus and can be overridden locally. ## What replaced the raw key API | Deprecated in 3.19 | Replacement | |---|---| | `RawKeyEvent`, `RawKeyDownEvent`, `RawKeyUpEvent` | `KeyEvent`, `KeyDownEvent`, `KeyRepeatEvent`, `KeyUpEvent` | | `RawKeyboard.instance.addListener` | `HardwareKeyboard.instance.addHandler` | | `RawKeyboardListener` | `KeyboardListener` | | `Focus.onKey`, `FocusNode.onKey` | `onKeyEvent` | | `RawKeyEvent.isControlPressed` and friends | `HardwareKeyboard.instance.isControlPressed` and friends | The raw API exposed platform-specific data (`RawKeyEventData` subclasses per OS); the new model drops that in favour of one cross-platform event with explicit repeat events. ## Common mistakes - Reacting to both `KeyDownEvent` and `KeyUpEvent`, so a command runs twice per press. Filter on `event is KeyDownEvent` (and decide whether `KeyRepeatEvent` should count). - Matching on `physicalKey` for a shortcut letter, which breaks on non-QWERTY layouts. - Returning `handled` for every key from a wrapper `Focus`, which starves ancestors such as `Shortcuts` and Tab traversal. - Adding a `HardwareKeyboard` handler in `initState` and never removing it, so a disposed widget keeps receiving keys.
- In Flutter, when should a shortcut compare logicalKey rather than physicalKey?Compare `logicalKey` for anything named by a letter or symbol — Ctrl+P should follow the P the user sees, whatever the layout. Compare `physicalKey` when position matters more than meaning, such as movement keys in a game, which should stay under the same fingers on AZERTY and QWERTY keyboards.
- Does returning true from a HardwareKeyboard.instance handler stop Focus.onKeyEvent handlers from seeing the key?No. Every registered `HardwareKeyboard` handler runs regardless of the others' results, and the event is then dispatched to the focus tree anyway. Returning `true` only marks the event handled for the engine, so it is not passed on to native components. To stop a key inside Flutter, handle it in the focus tree with `KeyEventResult.handled`.
saying these in an interview costs you the question
- RawKeyboardListener is still the recommended widget for reading keys
- Returning true from a HardwareKeyboard handler stops the focus tree seeing the key
- Focus.onKeyEvent receives keys even when focus is elsewhere in the app
- skipRemainingHandlers and handled mean the same thing
- physicalKey is the right field for letter shortcuts on every layout