Introduzione

Il drag & drop è un'interazione familiare per gli utenti: trascinare un elemento da un punto all'altro rende le interfacce più intuitive e coinvolgenti. Pensiamo a una board in stile Kanban, a un carrello della spesa dove si trascinano prodotti, o a una lista riordinabile.

Flutter offre un supporto nativo e completo per queste interazioni tramite i widget Draggable, DragTarget e LongPressDraggable, oltre al comodo ReorderableListView per i casi più comuni. In questa guida vedremo come usarli in pratica.

I widget fondamentali

Il sistema di drag & drop di Flutter si basa su due componenti principali:

  • Draggable: rende un widget trascinabile. Trasporta un valore (il data) che verrà consegnato alla destinazione.
  • DragTarget: definisce una zona in cui è possibile rilasciare gli elementi trascinati, decidendo se accettarli o meno.

Un esempio base con Draggable

Il widget Draggable richiede tre proprietà chiave: data (il valore trasportato), child (l'aspetto normale) e feedback (l'aspetto durante il trascinamento).

Draggable<int>(
  data: 10,
  feedback: Material(
    color: Colors.transparent,
    child: Container(
      width: 80,
      height: 80,
      color: Colors.blue.withOpacity(0.7),
      child: const Center(
        child: Text('Trascina', style: TextStyle(color: Colors.white)),
      ),
    ),
  ),
  childWhenDragging: Container(
    width: 80,
    height: 80,
    color: Colors.grey.shade300,
  ),
  child: Container(
    width: 80,
    height: 80,
    color: Colors.blue,
    child: const Center(
      child: Text('Trascina', style: TextStyle(color: Colors.white)),
    ),
  ),
)

Alcune note importanti:

  • Il feedback viene renderizzato al di fuori dell'albero normale, quindi va spesso avvolto in un Material per evitare artefatti visivi come il testo con sottolineatura gialla.
  • childWhenDragging sostituisce il widget originale durante il trascinamento (opzionale).

Ricevere gli elementi con DragTarget

Il DragTarget costruisce la propria UI in base agli elementi che stanno per essere rilasciati e reagisce quando avviene il drop.

int _totale = 0;

DragTarget<int>(
  builder: (context, candidateData, rejectedData) {
    final isHovering = candidateData.isNotEmpty;
    return Container(
      width: 150,
      height: 150,
      color: isHovering ? Colors.green.shade200 : Colors.green.shade50,
      child: Center(
        child: Text('Totale: $_totale'),
      ),
    );
  },
  onWillAcceptWithDetails: (details) => details.data > 0,
  onAcceptWithDetails: (details) {
    setState(() {
      _totale += details.data;
    });
  },
)

I callback principali sono:

  • builder: riceve candidateData (gli elementi accettabili sopra il target) e rejectedData (quelli rifiutati). Utile per dare feedback visivo durante l'hover.
  • onWillAcceptWithDetails: decide se accettare o meno l'elemento in arrivo. Restituendo false, l'elemento finirà tra i rejectedData.
  • onAcceptWithDetails: viene invocato quando l'elemento viene effettivamente rilasciato.

Nota: i vecchi callback onWillAccept e onAccept sono deprecati a favore delle versioni ...WithDetails, che forniscono anche la posizione del drop tramite details.offset.

LongPressDraggable per le liste

Quando gli elementi trascinabili sono dentro una lista scorrevole, un Draggable normale entrerebbe in conflitto con lo scroll. La soluzione è LongPressDraggable, che avvia il trascinamento solo dopo una pressione prolungata.

LongPressDraggable<Task>(
  data: task,
  feedback: Material(
    elevation: 6,
    child: TaskCard(task: task),
  ),
  child: TaskCard(task: task),
)

Riordinare liste con ReorderableListView

Per il caso comune del riordino di una lista, Flutter offre ReorderableListView, che gestisce internamente tutta la logica di drag & drop.

ReorderableListView(
  onReorder: (oldIndex, newIndex) {
    setState(() {
      if (newIndex > oldIndex) {
        newIndex -= 1;
      }
      final item = _items.removeAt(oldIndex);
      _items.insert(newIndex, item);
    });
  },
  children: [
    for (final item in _items)
      ListTile(
        key: ValueKey(item),
        title: Text(item),
      ),
  ],
)

Attenzione a due dettagli:

  • Ogni figlio deve avere una key univoca, altrimenti Flutter non riesce a tracciare gli spostamenti.
  • La correzione newIndex -= 1 quando newIndex > oldIndex è necessaria perché l'elemento viene prima rimosso e poi reinserito.

Best practice

  • Feedback chiaro: usa childWhenDragging e cambia lo stile del DragTarget durante l'hover per comunicare all'utente cosa sta accadendo.
  • Material per il feedback: avvolgi sempre il feedback in un Material per evitare problemi di rendering del testo.
  • Accessibilità: il drag & drop puro non è sempre accessibile; considera alternative (come pulsanti di spostamento) per gli utenti che usano screen reader.
  • Prestazioni: mantieni leggero il widget feedback, poiché viene ricostruito durante il movimento.

Conclusioni

Flutter rende il drag & drop sorprendentemente semplice da implementare. Con Draggable e DragTarget hai il controllo completo su interazioni personalizzate, mentre ReorderableListView copre il caso d'uso più frequente del riordino con poche righe di codice. Sperimenta con i feedback visivi per creare interfacce davvero fluide e intuitive.