Selezionare più elementi in una lista Flutter con selezione multipla e AppBar contestuale

Foto di Shubham Dhage su Unsplash

GuideIntermedio35 min Flutter 3.x

Selezionare più elementi in una lista Flutter con selezione multipla e AppBar contestuale

Introduzione

La selezione multipla è un pattern UX molto comune nelle app mobili: pensa a Gmail, Google Foto o l'app File. L'utente attiva la modalità selezione con un tocco prolungato, poi seleziona più elementi e applica un'azione di gruppo (eliminare, condividere, archiviare).

In questo tutorial costruiremo da zero una lista di elementi con:

  • Attivazione della modalità selezione tramite tocco prolungato (long press)
  • Feedback visivo sugli elementi selezionati
  • Una AppBar contestuale che mostra il conteggio e le azioni disponibili
  • Gestione pulita dello stato con un semplice StatefulWidget

Non useremo pacchetti esterni: solo widget nativi Flutter. Al termine avrai un componente riutilizzabile per qualsiasi lista che richieda operazioni batch.

  1. 1

    Preparare il modello dati e la schermata di base

    Iniziamo definendo un semplice modello Task e la struttura della schermata. Ogni elemento avrà un identificativo e un titolo. Generiamo una lista di esempio per lavorare subito con dati concreti.

    Usiamo uno StatefulWidget perché dovremo tenere traccia dello stato della selezione, che cambierà durante l'interazione dell'utente.

    class Task {
      final int id;
      final String title;
    
      const Task({required this.id, required this.title});
    }
    
    class MultiSelectPage extends StatefulWidget {
      const MultiSelectPage({super.key});
    
      @override
      State<MultiSelectPage> createState() => _MultiSelectPageState();
    }
    
    class _MultiSelectPageState extends State<MultiSelectPage> {
      final List<Task> _tasks = List.generate(
        20,
        (i) => Task(id: i, title: 'Attività numero $i'),
      );
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Le mie attività')),
          body: ListView.builder(
            itemCount: _tasks.length,
            itemBuilder: (context, index) {
              final task = _tasks[index];
              return ListTile(title: Text(task.title));
            },
          ),
        );
      }
    }

    Risultato atteso

    Una schermata con una AppBar e una lista scrollabile di 20 attività, senza alcuna selezione ancora attiva.

  2. 2

    Aggiungere lo stato della selezione

    Introduciamo un Set<int> per memorizzare gli id degli elementi selezionati. Usiamo un Set invece di una List perché garantisce l'unicità e offre operazioni di aggiunta/rimozione efficienti.

    Deriviamo due proprietà utili:

    • _isSelectionMode: vera quando c'è almeno un elemento selezionato
    • _isSelected(id): verifica se un dato elemento è selezionato

    Aggiungiamo anche un metodo _toggleSelection che aggiunge o rimuove un id, chiamato dentro setState per aggiornare la UI.

    final Set<int> _selectedIds = {};
    
    bool get _isSelectionMode => _selectedIds.isNotEmpty;
    
    bool _isSelected(int id) => _selectedIds.contains(id);
    
    void _toggleSelection(int id) {
      setState(() {
        if (_selectedIds.contains(id)) {
          _selectedIds.remove(id);
        } else {
          _selectedIds.add(id);
        }
      });
    }
    
    void _clearSelection() {
      setState(() => _selectedIds.clear());
    }

    Risultato atteso

    Il codice compila e lo stato della selezione è pronto per essere usato, anche se non è ancora collegato all'interfaccia.

  3. 3

    Gestire long press e tocco negli elementi della lista

    Ora colleghiamo l'interazione. La logica è questa:

    • Il long press su un elemento attiva la modalità selezione e seleziona quell'elemento.
    • Un tocco singolo ha comportamento diverso a seconda della modalità: se la selezione è attiva, aggiunge/rimuove l'elemento; altrimenti apre il dettaglio (o compie l'azione normale).

    Aggiungiamo anche un feedback visivo: mostriamo una Checkbox come leading quando la selezione è attiva e coloriamo la riga selezionata.

    Widget _buildTile(Task task) {
      final selected = _isSelected(task.id);
      return ListTile(
        onLongPress: () => _toggleSelection(task.id),
        onTap: () {
          if (_isSelectionMode) {
            _toggleSelection(task.id);
          } else {
            // azione normale, es. apertura dettaglio
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Apri ${task.title}')),
            );
          }
        },
        selected: selected,
        selectedTileColor: Theme.of(context).colorScheme.primaryContainer,
        leading: _isSelectionMode
            ? Checkbox(
                value: selected,
                onChanged: (_) => _toggleSelection(task.id),
              )
            : CircleAvatar(child: Text('${task.id}')),
        title: Text(task.title),
      );
    }

    Risultato atteso

    Tenendo premuto un elemento appare la checkbox e la riga si evidenzia. In modalità selezione, i tocchi selezionano/deselezionano gli elementi.

  4. 4

    Creare la AppBar contestuale

    Quando la selezione è attiva, sostituiamo la AppBar standard con una AppBar contestuale che mostra:

    • Un pulsante chiudi (X) per uscire dalla modalità selezione
    • Il conteggio degli elementi selezionati come titolo
    • Le azioni di gruppo (elimina, seleziona tutto) negli actions

    Usiamo una funzione che restituisce la AppBar giusta in base a _isSelectionMode. Poiché entrambe implementano PreferredSizeWidget, possiamo assegnarle direttamente alla proprietà appBar dello Scaffold.

    PreferredSizeWidget _buildAppBar() {
      if (!_isSelectionMode) {
        return AppBar(title: const Text('Le mie attività'));
      }
    
      return AppBar(
        backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: _clearSelection,
        ),
        title: Text('${_selectedIds.length} selezionati'),
        actions: [
          IconButton(
            icon: const Icon(Icons.select_all),
            tooltip: 'Seleziona tutto',
            onPressed: _selectAll,
          ),
          IconButton(
            icon: const Icon(Icons.delete),
            tooltip: 'Elimina',
            onPressed: _deleteSelected,
          ),
        ],
      );
    }
    
    void _selectAll() {
      setState(() {
        _selectedIds.addAll(_tasks.map((t) => t.id));
      });
    }

    Risultato atteso

    Attivando la selezione, la AppBar cambia colore e mostra il conteggio con i pulsanti di azione. Il pulsante X annulla la selezione.

  5. 5

    Implementare l'azione di eliminazione di gruppo

    Colleghiamo il pulsante elimina a una funzione che rimuove tutti gli elementi selezionati dalla lista. Dopo l'eliminazione svuotiamo la selezione (uscendo automaticamente dalla modalità) e mostriamo uno SnackBar di conferma.

    Rimuoviamo gli elementi con removeWhere, controllando l'appartenenza al set degli id selezionati.

    void _deleteSelected() {
      final count = _selectedIds.length;
      setState(() {
        _tasks.removeWhere((task) => _selectedIds.contains(task.id));
        _selectedIds.clear();
      });
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('$count elementi eliminati')),
      );
    }

    Risultato atteso

    Selezionando alcuni elementi e premendo il cestino, questi vengono rimossi dalla lista e appare un messaggio di conferma.

  6. 6

    Gestire il tasto indietro e montare tutto insieme

    Un ultimo dettaglio UX importante: quando la selezione è attiva, il tasto indietro del dispositivo (o il gesto di ritorno) dovrebbe annullare la selezione invece di uscire dalla schermata. Usiamo PopScope per intercettare il tentativo di pop.

    Ecco il build finale che assembla la AppBar contestuale, la lista e la gestione del back.

    @override
    Widget build(BuildContext context) {
      return PopScope(
        canPop: !_isSelectionMode,
        onPopInvokedWithResult: (didPop, result) {
          if (!didPop && _isSelectionMode) {
            _clearSelection();
          }
        },
        child: Scaffold(
          appBar: _buildAppBar(),
          body: ListView.builder(
            itemCount: _tasks.length,
            itemBuilder: (context, index) => _buildTile(_tasks[index]),
          ),
        ),
      );
    }

    Risultato atteso

    Con la selezione attiva, premere indietro annulla la selezione senza chiudere la schermata. L'app offre ora un'esperienza di selezione multipla completa e coerente.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!