Using Keyboard Shortcuts in Flutter Desktop

2023-07-15
#dart #flutter

Intro

Using keyboard shortcuts in desktop is a common case, it can boost our productivity when used correctly. In flutter, there're two ways for us to build keyboard shortcuts in desktop app. One is RawKeyboardListener and the another is Shortcuts, but they have significant differences.

RawKeyboardListener

RawKeyboardListener is useful for listening to raw key events and hardware buttons that are represented as keys. Typically used by games and other apps that use keyboards for purposes other than text entry.
It allows you to listen to "key-up" and "key-down" events on the keyboard

  @override
   Widget build(BuildContext context) {
     return KeyboardListener(
         autofocus: true,
         onKeyEvent: (event) {
           if (event.runtimeType == RawKeyDownEvent) {
           } else {
             
           }
         },
         focusNode: FocusNode(),
         child: child,
      );
   }

Undoubtedly, we can use this widget to implement keyboard shortcuts. But in reality, it's a bit complex, and most of the time, we don't want to filter keydown events.

More importantly, if you want to use shortcuts with menu bar plugin, you will need to use LogicalKeySet, while RawKeyboardListener is not based on this.

Moreover, RawKeyboardListener may not work as expected sometimes, for example, when you have to deal with hotkeys with modifiers Ctrl or Alt. Code may like this:

  RawKeyboardListener(
          autofocus: true,
          onKey: (event) {
            if (event.runtimeType == RawKeyDownEvent) {
               print(event.logicalKey);
              if (event.isKeyPressed(LogicalKeyboardKey.control) &&
                  event.isKeyPressed(LogicalKeyboardKey.keyC)) {
                _incrementCounter();
              }
            }
          },
          focusNode: FocusNode(),
          child: child
    )

But actually, event.logicalKey will print Control Left or Control Right and event.isKeyPressed(LogicalKeyboardKey.control) will never return true. For that purpose, you need to recorrect code like:

  ...  
  if ((event.isKeyPressed(LogicalKeyboardKey.controlLeft) ||  event.isKeyPressed(LogicalKeyboardKey.controlLeft) &&
                  event.isKeyPressed(LogicalKeyboardKey.keyC)) {
                _incrementCounter();
              }

Therefore, if you want to use keyboard shortcuts in desktop applications built using Flutter, you should consider the Shortcuts widget. It's a simpler and more convenient way to create keyboard shortcuts in Flutter.

Shortcuts

A widget that creates key bindings to specific actions for its descendants. See the article on Using Actions and Shortcuts for a detailed explanation.
It takes in a map of shortcuts (LogicalKeySet: Intent), and actions (Intent: Action).

Intents and Actions

An intent always represents particular configuration of an Action. Take the example of a desktop with a keyboard shortcuts that Increase/decrease. An intent for this would like this:

  class IncrementIntent extends Intent {
    const IncrementIntent();
  }
  
  class DecrementIntent extends Intent {
    const DecrementIntent();
  }
  

And what action does, it will do something once an Intent is received. In our copy example, an action for the intent would look like this:

  Actions(
          actions: <Type, Action<Intent>>{
            IncrementIntent: CallbackAction<IncrementIntent>(
              onInvoke: (IncrementIntent intent) => setState(() {
                count = count + 1;
              }),
            ),
            DecrementIntent: CallbackAction<DecrementIntent>(
              onInvoke: (DecrementIntent intent) => setState(() {
                count = count - 1;
              }),
            ),
          }

You can define different types of actions and intents for different scenarios.

Example Using Shortcuts

source code comes from Shortcuts  Class

  import 'package:flutter/material.dart';
  import 'package:flutter/services.dart';
  
  /// Flutter code sample for [Shortcuts].
  
  void main() => runApp(const ShortcutsExampleApp());
  
  class ShortcutsExampleApp extends StatelessWidget {
    const ShortcutsExampleApp({super.key});
  
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Shortcuts Sample')),
          body: const Center(
            child: ShortcutsExample(),
          ),
        ),
      );
    }
  }
  
  class IncrementIntent extends Intent {
    const IncrementIntent();
  }
  
  class DecrementIntent extends Intent {
    const DecrementIntent();
  }
  
  class ShortcutsExample extends StatefulWidget {
    const ShortcutsExample({super.key});
  
    @override
    State<ShortcutsExample> createState() => _ShortcutsExampleState();
  }
  
  class _ShortcutsExampleState extends State<ShortcutsExample> {
    int count = 0;
  
    @override
    Widget build(BuildContext context) {
      return Shortcuts(
        shortcuts: const <ShortcutActivator, Intent>{
          SingleActivator(LogicalKeyboardKey.arrowUp): IncrementIntent(),
          SingleActivator(LogicalKeyboardKey.arrowDown): DecrementIntent(),
        },
        child: Actions(
          actions: <Type, Action<Intent>>{
            IncrementIntent: CallbackAction<IncrementIntent>(
              onInvoke: (IncrementIntent intent) => setState(() {
                count = count + 1;
              }),
            ),
            DecrementIntent: CallbackAction<DecrementIntent>(
              onInvoke: (DecrementIntent intent) => setState(() {
                count = count - 1;
              }),
            ),
          },
          child: Focus(
            autofocus: true,
            child: Column(
              children: <Widget>[
                const Text('Add to the counter by pressing the up arrow key'),
                const Text(
                    'Subtract from the counter by pressing the down arrow key'),
                Text('count: $count'),
              ],
            ),
          ),
        ),
      );
    }
  }
  

RawKeyboardListener vs Shortcuts

In summary, RawKeyboardListener is more powerful than Shortcuts, it can listen all keyboard event, include "key-up" and "key-down" events, among others. However, this also means RawKeyboardListener is more complex than Shortcuts. On the other hand, Shortcuts is easier to use and make code clearer. After all, you define all the action and intent at first.

Reference