Liste in Flutter con ListView.builder e ListTile: la guida per iniziare

Foto di Thought Catalog su Unsplash

GuidePrincipiante35 min Flutter 3.x

Liste in Flutter con ListView.builder e ListTile: la guida per iniziare

Quasi ogni app mostra una lista: contatti, prodotti, messaggi, attività da fare. In Flutter la lista è un widget come tutti gli altri, e imparare a usarlo bene è uno dei primi passi fondamentali.

In questo tutorial partiamo da una semplice lista di dati in memoria e costruiamo passo dopo passo una schermata scorrevole usando ListView.builder (la variante efficiente, che costruisce solo gli elementi visibili) e ListTile (il widget pronto all'uso per le righe di una lista).

Alla fine saprai:

  • la differenza tra ListView "statica" e ListView.builder;
  • come comporre una riga con ListTile (icona, titolo, sottotitolo, azione);
  • come aggiungere separatori con ListView.separated;
  • come reagire al tocco su un elemento;
  • come evitare i classici errori (ListView dentro Column, altezza illimitata, overflow).

Serve solo un progetto Flutter funzionante (flutter create liste_demo) e un editor. Nessun pacchetto esterno.

  1. 1

    Preparare il progetto e i dati di esempio

    Crea un nuovo progetto (flutter create liste_demo) e svuota il contenuto di lib/main.dart.

    Prima della UI serve qualcosa da mostrare: definiamo una piccola classe Contatto e una lista in memoria. Lavorare con un modello tipizzato (invece che con Map generiche) rende il codice più leggibile e ti fa scoprire subito gli errori grazie alla null safety.

    Nota l'uso di final per i campi e del costruttore con parametri required: sono buone abitudini che ti eviteranno molti problemi più avanti.

    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class Contatto {
      final String nome;
      final String ruolo;
      final String iniziali;
    
      const Contatto({
        required this.nome,
        required this.ruolo,
        required this.iniziali,
      });
    }
    
    const List<Contatto> contatti = [
      Contatto(nome: 'Giulia Rossi', ruolo: 'Product Manager', iniziali: 'GR'),
      Contatto(nome: 'Marco Bianchi', ruolo: 'Flutter Developer', iniziali: 'MB'),
      Contatto(nome: 'Sara Conti', ruolo: 'UI Designer', iniziali: 'SC'),
      Contatto(nome: 'Luca Ferrari', ruolo: 'Backend Developer', iniziali: 'LF'),
      Contatto(nome: 'Elena Moretti', ruolo: 'QA Engineer', iniziali: 'EM'),
    ];
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Liste Demo',
          theme: ThemeData(
            colorSchemeSeed: Colors.indigo,
            useMaterial3: true,
          ),
          home: const ContattiPage(),
        );
      }
    }
    
    class ContattiPage extends StatelessWidget {
      const ContattiPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Contatti')),
          body: const Center(child: Text('Qui arriverà la lista')),
        );
      }
    }

    Risultato atteso

    L'app si avvia e mostra una AppBar con il titolo "Contatti" e il testo segnaposto al centro. Nessun errore in console.

  2. 2

    La ListView "statica": comoda ma limitata

    Il modo più semplice di creare una lista è il costruttore ListView(children: [...]). Riceve una lista di widget già pronti e li rende scorrevoli.

    È perfetto quando gli elementi sono pochi e conosciuti a priori (per esempio le voci di una schermata Impostazioni), ma ha un difetto importante: costruisce tutti i figli subito, anche quelli fuori dallo schermo. Con 1.000 elementi (o con una lista che arriva dal server) l'app diventerebbe lenta e sprecherebbe memoria.

    Sostituisci il body dello Scaffold con il codice qui sotto per vedere il risultato, poi passa allo step successivo per la versione corretta.

    // Dentro ContattiPage.build
    return Scaffold(
      appBar: AppBar(title: const Text('Contatti')),
      body: ListView(
        children: [
          for (final c in contatti)
            ListTile(
              title: Text(c.nome),
              subtitle: Text(c.ruolo),
            ),
        ],
      ),
    );

    Risultato atteso

    Vedi i 5 contatti in un elenco scorrevole. Funziona, ma tutti gli elementi vengono costruiti insieme: va bene solo per liste corte.

  3. 3

    Passare a ListView.builder

    ListView.builder è la versione lazy: costruisce gli elementi solo quando stanno per entrare nell'area visibile e li ricicla durante lo scroll.

    Richiede due parametri chiave:

    • itemCount: quanti elementi ha la lista (se lo ometti, la lista è considerata infinita);
    • itemBuilder: una funzione (BuildContext context, int index) che restituisce il widget per l'elemento in posizione index.

    Dentro itemBuilder recuperi l'oggetto con contatti[index] e lo trasformi in una riga. È esattamente il pattern che userai per i dati che arrivano da un'API o da un database.

    class ContattiPage extends StatelessWidget {
      const ContattiPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Contatti')),
          body: ListView.builder(
            itemCount: contatti.length,
            itemBuilder: (context, index) {
              final contatto = contatti[index];
              return ListTile(
                title: Text(contatto.nome),
                subtitle: Text(contatto.ruolo),
              );
            },
          ),
        );
      }
    }

    Risultato atteso

    La lista appare identica a prima, ma ora è efficiente: prova a cambiare i dati con `List.generate(1000, ...)` e lo scroll resterà fluido.

  4. 4

    Rendere le righe complete con ListTile

    ListTile è il widget Material pensato per le righe di lista. Le sue proprietà più usate:

    • leading: widget a sinistra (icona, avatar, immagine);
    • title: il testo principale;
    • subtitle: il testo secondario;
    • trailing: widget a destra (freccia, switch, badge);
    • onTap / onLongPress: le interazioni;
    • selected, dense, contentPadding: personalizzazioni rapide.

    Aggiungiamo un CircleAvatar con le iniziali e una freccia a destra. Nota overflow: TextOverflow.ellipsis sul titolo: è la difesa più semplice contro i nomi lunghi che sforerebbero lo spazio disponibile.

    itemBuilder: (context, index) {
      final contatto = contatti[index];
      return ListTile(
        leading: CircleAvatar(
          backgroundColor: Theme.of(context).colorScheme.primaryContainer,
          child: Text(contatto.iniziali),
        ),
        title: Text(
          contatto.nome,
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
        ),
        subtitle: Text(contatto.ruolo),
        trailing: const Icon(Icons.chevron_right),
        onTap: () {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Hai toccato ${contatto.nome}')),
          );
        },
      );
    },

    Risultato atteso

    Ogni riga mostra avatar con iniziali, nome, ruolo e freccia. Toccando una riga compare in basso una SnackBar con il nome del contatto.

  5. 5

    Separatori e header con ListView.separated

    Per inserire una linea (o qualsiasi widget) tra un elemento e l'altro, non aggiungere un Divider dentro l'itemBuilder: useresti un separatore anche dopo l'ultimo elemento. La soluzione corretta è ListView.separated, che aggiunge separatorBuilder al set di parametri già visti.

    Aggiungiamo anche un po' di respiro con padding e mostriamo come gestire il caso della lista vuota: un controllo con if (contatti.isEmpty) evita di presentare una schermata bianca all'utente, uno degli scivoloni più frequenti nelle prime app.

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(title: const Text('Contatti')),
        body: contatti.isEmpty
            ? const Center(child: Text('Nessun contatto disponibile'))
            : ListView.separated(
                padding: const EdgeInsets.symmetric(vertical: 8),
                itemCount: contatti.length,
                separatorBuilder: (context, index) => const Divider(
                  height: 1,
                  indent: 72,
                ),
                itemBuilder: (context, index) {
                  final contatto = contatti[index];
                  return ListTile(
                    leading: CircleAvatar(child: Text(contatto.iniziali)),
                    title: Text(contatto.nome,
                        maxLines: 1, overflow: TextOverflow.ellipsis),
                    subtitle: Text(contatto.ruolo),
                    trailing: const Icon(Icons.chevron_right),
                    onTap: () {},
                  );
                },
              ),
      );
    }

    Risultato atteso

    Tra una riga e l'altra compare una sottile linea allineata al testo (grazie a `indent: 72`), senza linea finale dopo l'ultimo elemento.

  6. 6

    Evitare i due errori classici: ListView dentro Column e altezza illimitata

    Appena provi a mettere un titolo sopra la lista dentro una Column, incontri il famigerato errore:

    RenderBox was not laid out oppure Vertical viewport was given unbounded height.

    Il motivo è che la Column dà ai figli altezza "illimitata", mentre la ListView vuole sapere quanto spazio occupare. Hai tre soluzioni:

    1. Expanded (consigliata): la lista prende tutto lo spazio rimanente e resta scorrevole.
    2. shrinkWrap: true + physics: NeverScrollableScrollPhysics(): la lista si dimensiona sul contenuto e lo scroll lo gestisce il genitore (SingleChildScrollView). Usalo solo con poche righe: perde il vantaggio del lazy loading.
    3. CustomScrollView con SliverList: la soluzione più flessibile per layout complessi.

    Altro errore frequente: mettere una Row con testi lunghi dentro un ListTile senza Expanded, ottenendo le classiche strisce gialle e nere di overflow. Avvolgi il testo in Expanded e usa overflow: TextOverflow.ellipsis.

    Ultimo consiglio: quando la lista è modificabile (elementi aggiunti, rimossi o riordinati), assegna una key stabile agli elementi, ad esempio key: ValueKey(contatto.id), così Flutter riconosce le righe e non mescola gli stati.

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(title: const Text('Contatti')),
        body: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Padding(
              padding: EdgeInsets.all(16),
              child: Text(
                'Team di progetto',
                style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
              ),
            ),
            // ✅ Expanded risolve l'altezza illimitata
            Expanded(
              child: ListView.builder(
                itemCount: contatti.length,
                itemBuilder: (context, index) {
                  final contatto = contatti[index];
                  return ListTile(
                    key: ValueKey(contatto.nome),
                    leading: CircleAvatar(child: Text(contatto.iniziali)),
                    title: Row(
                      children: [
                        // ✅ Expanded evita l'overflow orizzontale
                        Expanded(
                          child: Text(
                            contatto.nome,
                            maxLines: 1,
                            overflow: TextOverflow.ellipsis,
                          ),
                        ),
                        const SizedBox(width: 8),
                        const Icon(Icons.star_border, size: 18),
                      ],
                    ),
                    subtitle: Text(contatto.ruolo),
                    onTap: () {},
                  );
                },
              ),
            ),
          ],
        ),
      );
    }

    Risultato atteso

    La schermata mostra un titolo fisso in alto e sotto la lista scorrevole, senza errori di layout né strisce gialle e nere di overflow.

  7. 7

    Ricapitolare e proseguire

    Hai costruito una lista completa partendo da zero. I punti da ricordare:

    • usa ListView(children: [...]) solo per pochi elementi fissi;
    • usa ListView.builder con itemCount + itemBuilder per liste lunghe o dinamiche;
    • ListView.separated aggiunge separatori nel modo corretto;
    • ListTile copre il 90% dei casi con leading, title, subtitle, trailing e onTap;
    • dentro una Column avvolgi sempre la lista in Expanded;
    • gestisci lo stato vuoto e usa TextOverflow.ellipsis per i testi lunghi.

    Esercizi per fissare i concetti:

    1. Genera 500 contatti con List.generate e verifica che lo scroll resti fluido.
    2. Aggiungi un Switch come trailing per marcare i preferiti (ti servirà uno StatefulWidget e setState).
    3. Apri una schermata di dettaglio al tocco con Navigator.push.
    4. Prova ListView.builder(scrollDirection: Axis.horizontal) per una lista orizzontale di card.
    // Esercizio 1: lista lunga generata al volo
    final contattiLunghi = List.generate(
      500,
      (i) => Contatto(
        nome: 'Contatto n. $i',
        ruolo: i.isEven ? 'Sviluppatore' : 'Designer',
        iniziali: 'C$i',
      ),
    );

    Risultato atteso

    Con 500 elementi la lista scorre senza rallentamenti: è la conferma che `ListView.builder` costruisce solo le righe visibili.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!