Drag & drop tra liste diverse in Flutter con Draggable e DragTarget

Foto di Pete Lead su Unsplash

GuideIntermedio35 min Flutter 3.x

Drag & drop tra liste diverse in Flutter con Draggable e DragTarget

Il drag & drop è un pattern di interazione molto potente per rendere le tue app più dinamiche e intuitive. Pensa a una board Kanban (come Trello) dove trascini le card tra colonne "Da fare", "In corso" e "Completato".

In questo tutorial useremo i widget nativi di Flutter Draggable e DragTarget, senza pacchetti esterni, per costruire una mini board Kanban con tre colonne e la possibilità di spostare le card da una colonna all'altra semplicemente trascinandole.

Al termine avrai capito come funziona il feedback visivo durante il trascinamento e come aggiornare lo stato quando un elemento viene rilasciato.

  1. 1

    Definire il modello dei dati

    Iniziamo modellando i dati della nostra board. Ogni card è un semplice oggetto Task con un titolo. Ogni colonna è identificata da un enum TaskStatus.

    Usiamo una Map per raggruppare i task per stato: questa struttura ci renderà facile spostare un elemento da una colonna all'altra.

    enum TaskStatus { todo, inProgress, done }
    
    extension TaskStatusLabel on TaskStatus {
      String get label => switch (this) {
            TaskStatus.todo => 'Da fare',
            TaskStatus.inProgress => 'In corso',
            TaskStatus.done => 'Completato',
          };
    }
    
    class Task {
      final String id;
      final String title;
    
      const Task({required this.id, required this.title});
    }

    Risultato atteso

    Hai un modello Task e un enum TaskStatus con etichette leggibili per ogni colonna.

  2. 2

    Creare lo StatefulWidget della board

    Creiamo un StatefulWidget che mantiene lo stato della board: una mappa che associa ogni TaskStatus a una lista di Task.

    Definiamo anche un metodo _moveTask che rimuove un task dalla colonna di origine e lo aggiunge a quella di destinazione, chiamando setState per aggiornare l'interfaccia.

    class KanbanBoard extends StatefulWidget {
      const KanbanBoard({super.key});
    
      @override
      State<KanbanBoard> createState() => _KanbanBoardState();
    }
    
    class _KanbanBoardState extends State<KanbanBoard> {
      final Map<TaskStatus, List<Task>> _tasks = {
        TaskStatus.todo: [
          const Task(id: '1', title: 'Progettare la UI'),
          const Task(id: '2', title: 'Scrivere i test'),
        ],
        TaskStatus.inProgress: [
          const Task(id: '3', title: 'Implementare API'),
        ],
        TaskStatus.done: [
          const Task(id: '4', title: 'Setup progetto'),
        ],
      };
    
      void _moveTask(Task task, TaskStatus from, TaskStatus to) {
        if (from == to) return;
        setState(() {
          _tasks[from]!.removeWhere((t) => t.id == task.id);
          _tasks[to]!.add(task);
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return const Placeholder();
      }
    }

    Risultato atteso

    Hai lo stato iniziale della board con alcune card distribuite nelle tre colonne.

  3. 3

    Rendere le card trascinabili con Draggable

    Il widget Draggable<T> permette di trascinare un widget. I parametri fondamentali sono:

    • data: il dato associato al trascinamento (qui il Task).
    • feedback: il widget mostrato sotto il dito durante il trascinamento.
    • childWhenDragging: cosa mostrare al posto dell'originale mentre lo trascini.
    • child: l'aspetto normale della card.

    Costruiamo un metodo _buildCard che avvolge la card in un Draggable.

    Widget _buildCard(Task task) {
      final card = Card(
        margin: const EdgeInsets.symmetric(vertical: 6),
        child: Padding(
          padding: const EdgeInsets.all(12),
          child: Text(task.title),
        ),
      );
    
      return Draggable<Task>(
        data: task,
        feedback: Material(
          color: Colors.transparent,
          child: SizedBox(
            width: 200,
            child: Opacity(opacity: 0.9, child: card),
          ),
        ),
        childWhenDragging: Opacity(opacity: 0.3, child: card),
        child: card,
      );
    }

    Risultato atteso

    Le card ora possono essere trascinate e mostrano un feedback semitrasparente durante il trascinamento.

  4. 4

    Creare le zone di rilascio con DragTarget

    Ogni colonna deve accettare le card trascinate. Usiamo DragTarget<Task>, che espone tre callback utili:

    • onWillAcceptWithDetails: decide se accettare il dato (restituisce un bool).
    • onAcceptWithDetails: viene chiamato quando l'elemento viene rilasciato.
    • builder: costruisce la colonna e riceve la lista dei candidati in hover per cambiare aspetto visivo.

    Coloriamo la colonna quando una card sta per essere rilasciata sopra di essa.

    Widget _buildColumn(TaskStatus status) {
      final tasks = _tasks[status]!;
    
      return Expanded(
        child: DragTarget<Task>(
          onWillAcceptWithDetails: (details) => true,
          onAcceptWithDetails: (details) {
            final task = details.data;
            final from = _tasks.entries
                .firstWhere((e) => e.value.any((t) => t.id == task.id))
                .key;
            _moveTask(task, from, status);
          },
          builder: (context, candidate, rejected) {
            final isHovering = candidate.isNotEmpty;
            return Container(
              margin: const EdgeInsets.all(8),
              padding: const EdgeInsets.all(8),
              decoration: BoxDecoration(
                color: isHovering
                    ? Colors.blue.withOpacity(0.15)
                    : Colors.grey.withOpacity(0.08),
                borderRadius: BorderRadius.circular(12),
                border: Border.all(
                  color: isHovering ? Colors.blue : Colors.transparent,
                  width: 2,
                ),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Padding(
                    padding: const EdgeInsets.all(8),
                    child: Text(
                      status.label,
                      style: const TextStyle(fontWeight: FontWeight.bold),
                    ),
                  ),
                  Expanded(
                    child: ListView(
                      children: tasks.map(_buildCard).toList(),
                    ),
                  ),
                ],
              ),
            );
          },
        ),
      );
    }

    Risultato atteso

    Ogni colonna evidenzia il bordo blu quando trascini una card sopra di essa ed è pronta ad accettare il rilascio.

  5. 5

    Assemblare la board nel metodo build

    Ora mettiamo insieme le tre colonne in una Row all'interno dello Scaffold. Ogni colonna è già Expanded, quindi occuperanno lo spazio in modo equo.

    Aggiorniamo il metodo build del nostro _KanbanBoardState.

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(title: const Text('Kanban Board')),
        body: Row(
          children: TaskStatus.values.map(_buildColumn).toList(),
        ),
      );
    }

    Risultato atteso

    Vedi tre colonne affiancate e puoi trascinare le card da una all'altra: al rilascio la card cambia colonna.

  6. 6

    Avviare l'app e provare il drag & drop

    Infine colleghiamo la board a una MaterialApp e avviamo l'applicazione. Prova a trascinare una card da 'Da fare' a 'In corso' o 'Completato': lo stato si aggiornerà automaticamente grazie a setState.

    Suggerimento: per evitare eccezioni nella ricerca dello stato di origine, assicurati che ogni Task abbia un id univoco. Per una board più robusta potresti spostare la logica in un gestore di stato come Provider o Riverpod.

    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          debugShowCheckedModeBanner: false,
          home: KanbanBoard(),
        );
      }
    }

    Risultato atteso

    L'app si avvia mostrando la board Kanban funzionante: le card si spostano tra le colonne con il drag & drop.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!