skip to content

In Flutter, when do you read keys with Focus.onKeyEvent versus a HardwareKeyboard.instance handler, and what replaced RawKeyEvent?

level: seniorimportance: nice to knowfreq 24%

answer

  1. KeyDownEvent, KeyRepeatEvent, KeyUpEvent
  2. focus-scoped versus app-wide
  3. handled, ignored, skipRemainingHandlers
  4. logicalKey versus physicalKey
  5. RawKeyEvent deprecated in Flutter 3.19

basics

~10 s

Focus.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 s

Key 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 lines
dart
import '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

for a junior

Recall the three event classes, KeyDownEvent, KeyRepeatEvent and KeyUpEvent, and that Focus.onKeyEvent only fires while focus is inside it.

for a middle

Explain bubbling from the primary focus and the three KeyEventResult values, and when a global HardwareKeyboard handler is justified.

for a senior

Migrate raw key code, pick logical versus physical keys per feature, and keep global handlers from leaking past dispose.

for a principal

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