Gestire scorciatoie e azioni da tastiera in Flutter con Shortcuts e Actions

Foto di Gavin Phillips su Unsplash

GuideIntermedio35 min Flutter 3.x

Gestire scorciatoie e azioni da tastiera in Flutter con Shortcuts e Actions

Le applicazioni desktop e web professionali richiedono un supporto solido alla tastiera: scorciatoie come Ctrl+S per salvare o Ctrl+Z per annullare sono ormai uno standard che gli utenti si aspettano.

Flutter offre un sistema dichiarativo ed elegante per gestire le scorciatoie da tastiera basato su tre concetti chiave: Shortcuts (mappa combinazioni di tasti a Intent), Actions (mappa Intent a logica eseguibile) e Intent (l'intenzione astratta di un'azione).

In questo tutorial costruiremo un piccolo editor di testo con contatore che risponde a scorciatoie personalizzate, mostrandoti come separare in modo pulito la definizione dei tasti dalla logica applicativa.

  1. 1

    Comprendere il modello Intent / Shortcuts / Actions

    Prima di scrivere codice è importante capire i tre pezzi del puzzle:

    • Intent: una classe che rappresenta cosa vuoi fare (es. IncrementIntent). Non contiene logica, è solo un'etichetta.
    • Shortcuts: un widget che associa una combinazione di tasti (LogicalKeySet o SingleActivator) a un Intent.
    • Actions: un widget che associa un Intent a un Action, ovvero la logica che viene realmente eseguita.

    Questa separazione permette di riutilizzare gli stessi Intent con scorciatoie diverse su piattaforme diverse (ad esempio Cmd su macOS e Ctrl altrove).

    Definiamo per prima cosa i nostri Intent.

    import 'package:flutter/material.dart';
    
    // Ogni Intent rappresenta un'intenzione astratta.
    class IncrementIntent extends Intent {
      const IncrementIntent();
    }
    
    class DecrementIntent extends Intent {
      const DecrementIntent();
    }
    
    class ResetIntent extends Intent {
      const ResetIntent();
    }

    Risultato atteso

    Hai tre classi Intent pronte a essere collegate a scorciatoie e azioni.

  2. 2

    Creare lo scaffold dell'app con lo stato del contatore

    Costruiamo un semplice widget StatefulWidget che mantiene un contatore. In seguito collegheremo le scorciatoie ai metodi che modificano questo stato.

    Per ora l'interfaccia mostra solo il valore corrente e alcuni pulsanti tradizionali, così potrai confrontare i due approcci.

    class ShortcutDemo extends StatefulWidget {
      const ShortcutDemo({super.key});
    
      @override
      State<ShortcutDemo> createState() => _ShortcutDemoState();
    }
    
    class _ShortcutDemoState extends State<ShortcutDemo> {
      int _counter = 0;
    
      void _increment() => setState(() => _counter++);
      void _decrement() => setState(() => _counter--);
      void _reset() => setState(() => _counter = 0);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Shortcuts & Actions')),
          body: Center(
            child: Text(
              'Contatore: $_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ),
        );
      }
    }

    Risultato atteso

    Un'app con un contatore visualizzato al centro, per ora modificabile solo via codice.

  3. 3

    Definire le scorciatoie con il widget Shortcuts

    Avvolgiamo il contenuto con il widget Shortcuts, passando una mappa che associa i tasti agli Intent.

    Usiamo SingleActivator, il modo moderno e consigliato per descrivere una combinazione: accetta il tasto principale più i modificatori (control, shift, meta, alt).

    Nota: il widget Shortcuts deve avere un discendente con il focus per ricevere gli eventi. Useremo Focus con autofocus: true nel prossimo passo, oppure puoi usare FocusableActionDetector per semplificare.

    Shortcuts(
      shortcuts: <ShortcutActivator, Intent>{
        // freccia su / giù
        const SingleActivator(LogicalKeyboardKey.arrowUp): const IncrementIntent(),
        const SingleActivator(LogicalKeyboardKey.arrowDown): const DecrementIntent(),
        // Ctrl + R per il reset
        const SingleActivator(LogicalKeyboardKey.keyR, control: true):
            const ResetIntent(),
      },
      child: /* Actions + Focus verranno aggiunti dopo */ const SizedBox(),
    )

    Risultato atteso

    Le combinazioni di tasti sono mappate agli Intent, ma non producono ancora effetti perché mancano le Actions.

  4. 4

    Collegare gli Intent alla logica con il widget Actions

    Ora avvolgiamo il tutto con Actions, che associa ogni tipo di Intent a un Action.

    Usiamo CallbackAction<T>, la forma più semplice di Action: il suo onInvoke viene chiamato quando l'Intent corrispondente viene invocato.

    È fondamentale che il tipo generico dell'Action corrisponda al tipo dell'Intent nella mappa, altrimenti l'azione non verrà trovata.

    Actions(
      actions: <Type, Action<Intent>>{
        IncrementIntent: CallbackAction<IncrementIntent>(
          onInvoke: (intent) {
            _increment();
            return null;
          },
        ),
        DecrementIntent: CallbackAction<DecrementIntent>(
          onInvoke: (intent) {
            _decrement();
            return null;
          },
        ),
        ResetIntent: CallbackAction<ResetIntent>(
          onInvoke: (intent) {
            _reset();
            return null;
          },
        ),
      },
      child: /* Focus */ const SizedBox(),
    )

    Risultato atteso

    Gli Intent sono ora collegati alla logica, ma serve ancora un widget con il focus per ricevere gli eventi da tastiera.

  5. 5

    Dare il focus con Focus e assemblare l'albero

    Il sistema di scorciatoie funziona solo se un widget nell'albero ha il focus. Aggiungiamo un widget Focus con autofocus: true come figlio di Actions.

    Mettiamo insieme tutti i pezzi nel metodo build. L'ordine dall'esterno all'interno è: Shortcuts → Actions → Focus → UI.

    Ricorda di importare package:flutter/services.dart per accedere a LogicalKeyboardKey.

    import 'package:flutter/services.dart';
    
    @override
    Widget build(BuildContext context) {
      return Shortcuts(
        shortcuts: <ShortcutActivator, Intent>{
          const SingleActivator(LogicalKeyboardKey.arrowUp): const IncrementIntent(),
          const SingleActivator(LogicalKeyboardKey.arrowDown): const DecrementIntent(),
          const SingleActivator(LogicalKeyboardKey.keyR, control: true):
              const ResetIntent(),
        },
        child: Actions(
          actions: <Type, Action<Intent>>{
            IncrementIntent: CallbackAction<IncrementIntent>(
                onInvoke: (i) { _increment(); return null; }),
            DecrementIntent: CallbackAction<DecrementIntent>(
                onInvoke: (i) { _decrement(); return null; }),
            ResetIntent: CallbackAction<ResetIntent>(
                onInvoke: (i) { _reset(); return null; }),
          },
          child: Focus(
            autofocus: true,
            child: Scaffold(
              appBar: AppBar(title: const Text('Shortcuts & Actions')),
              body: Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text('Contatore: $_counter',
                        style: Theme.of(context).textTheme.headlineMedium),
                    const SizedBox(height: 12),
                    const Text('↑ incrementa  ·  ↓ decrementa  ·  Ctrl+R reset'),
                  ],
                ),
              ),
            ),
          ),
        ),
      );
    }

    Risultato atteso

    Avviando l'app su desktop o web, premendo le frecce e Ctrl+R il contatore reagisce alle scorciatoie da tastiera.

  6. 6

    Gestire i modificatori per piattaforma (Cmd vs Ctrl)

    Su macOS gli utenti usano il tasto Cmd (meta) al posto di Ctrl. Per offrire un'esperienza nativa possiamo aggiungere entrambe le scorciatoie oppure rilevare la piattaforma.

    Un approccio pulito è definire la mappa in modo condizionale usando defaultTargetPlatform. Ricorda di importare package:flutter/foundation.dart.

    È una buona pratica offrire scorciatoie coerenti con le convenzioni del sistema operativo su cui gira l'app.

    import 'package:flutter/foundation.dart';
    
    Map<ShortcutActivator, Intent> _buildShortcuts() {
      final bool isApple = defaultTargetPlatform == TargetPlatform.macOS;
      return <ShortcutActivator, Intent>{
        const SingleActivator(LogicalKeyboardKey.arrowUp): const IncrementIntent(),
        const SingleActivator(LogicalKeyboardKey.arrowDown): const DecrementIntent(),
        // Cmd+R su macOS, Ctrl+R altrove
        SingleActivator(
          LogicalKeyboardKey.keyR,
          control: !isApple,
          meta: isApple,
        ): const ResetIntent(),
      };
    }

    Risultato atteso

    Le scorciatoie usano automaticamente Cmd su macOS e Ctrl sulle altre piattaforme.

  7. 7

    Usare Action.overridable e invocare le azioni via codice

    A volte vuoi eseguire un'azione dal codice (ad esempio da un pulsante) riutilizzando la stessa logica delle scorciatoie. Puoi farlo con Actions.invoke o Actions.maybeInvoke, che cercano l'Action associata all'Intent nel contesto.

    Questo è utile per garantire che pulsanti e scorciatoie condividano esattamente lo stesso comportamento, evitando duplicazione di logica.

    Collega ad esempio un FloatingActionButton all'IncrementIntent.

    FloatingActionButton(
      onPressed: () {
        // Invoca l'Action registrata per IncrementIntent nel contesto corrente.
        Actions.maybeInvoke<IncrementIntent>(
          context,
          const IncrementIntent(),
        );
      },
      child: const Icon(Icons.add),
    )
    
    // In alternativa, per bottoni con il focus puoi usare:
    // Actions.invoke(context, const IncrementIntent());

    Risultato atteso

    Il pulsante e la scorciatoia da tastiera eseguono la stessa identica logica di incremento, mantenendo il codice DRY.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!