Gli errori tipici di chi inizia con Flutter: overflow, setState fuori posto e null safety

Foto di Artur Shamsutdinov su Unsplash

GuidePrincipiante35 min Flutter 3.x

Gli errori tipici di chi inizia con Flutter: overflow, setState fuori posto e null safety

Quando si inizia con Flutter, ci si scontra quasi sempre con gli stessi tre ostacoli: la fascia gialla e nera dell'overflow, l'errore setState() called after dispose() (o peggio, setState() during build) e i temuti errori di null safety come LateInitializationError o Null check operator used on a null value.

La buona notizia è che questi errori non sono bug del framework: sono messaggi che Flutter ti manda per dirti che stai violando una regola precisa. Una volta capita la regola, l'errore diventa banale da risolvere.

In questo tutorial ricreeremo volontariamente ciascun errore in un piccolo progetto di prova, leggeremo insieme il messaggio nella console e applicheremo la correzione corretta (non il classico "workaround" che nasconde il problema).

Livello: principiante. Serve solo un progetto Flutter funzionante e l'editor con il terminale aperto.

  1. 1

    Preparare un progetto di prova e imparare a leggere gli errori

    Creiamo un progetto pulito dove riprodurre gli errori in sicurezza.

    flutter create errori_flutter
    cd errori_flutter
    flutter run
    

    Prima di scrivere codice, tieni a mente dove guardare quando qualcosa va storto:

    • Sullo schermo: il riquadro rosso ("Red screen of death") in debug mostra il messaggio dell'eccezione di build.
    • Nella console (terminale o pannello Debug Console): il blocco che inizia con ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══ contiene la causa vera.
    • La riga più utile è quasi sempre quella che cita un file del tuo progetto (es. package:errori_flutter/main.dart:42), non i file interni di Flutter.

    Sostituisci il contenuto di lib/main.dart con lo scheletro qui sotto: nei prossimi passi cambieremo solo il corpo di HomePage.

    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Errori Flutter',
          theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
          home: const HomePage(),
        );
      }
    }
    
    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Laboratorio errori')),
          body: const Center(child: Text('Pronti a sbagliare!')),
        );
      }
    }

    Risultato atteso

    L'app si avvia mostrando una AppBar e il testo centrato "Pronti a sbagliare!". Console senza errori.

  2. 2

    Errore 1: RenderFlex overflowed — la fascia gialla e nera

    L'overflow avviene quando i figli di una Row o Column chiedono più spazio di quello disponibile. Flutter non ridimensiona automaticamente: te lo segnala.

    Prova questo body e osserva la fascia gialla/nera a destra:

    body: Row(
      children: [
        Container(width: 200, height: 100, color: Colors.red),
        Container(width: 200, height: 100, color: Colors.green),
        Container(width: 200, height: 100, color: Colors.blue),
      ],
    ),
    

    In console leggerai: A RenderFlex overflowed by 137 pixels on the right.

    Le tre soluzioni corrette, da scegliere in base all'intenzione:

    1. Expanded / Flexible: il figlio occupa lo spazio residuo. Usalo quando i widget devono dividersi lo spazio.
    2. SingleChildScrollView (o ListView): il contenuto deve poter scorrere, tipico quando appare la tastiera in un form.
    3. Per il testo lungo: avvolgilo in Expanded e usa maxLines + overflow: TextOverflow.ellipsis.

    ⚠️ Errore da non fare: mettere un Container con altezza infinita o usare Expanded dentro un widget che non è Row/Column/Flex → otterresti Incorrect use of ParentDataWidget.

    // VERSIONE CORRETTA
    body: Column(
      children: [
        // 1. I tre blocchi si dividono la larghezza disponibile
        Row(
          children: [
            Expanded(child: Container(height: 100, color: Colors.red)),
            Expanded(child: Container(height: 100, color: Colors.green)),
            Expanded(child: Container(height: 100, color: Colors.blue)),
          ],
        ),
        const SizedBox(height: 16),
        // 2. Testo lungo dentro una Row: Expanded + ellipsis
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Row(
            children: [
              const Icon(Icons.info_outline),
              const SizedBox(width: 8),
              Expanded(
                child: Text(
                  'Questo è un testo molto lungo che altrimenti causerebbe un overflow orizzontale nella Row.',
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
            ],
          ),
        ),
      ],
    ),

    Risultato atteso

    La fascia gialla e nera sparisce: i tre riquadri si adattano alla larghezza e il testo lungo viene troncato con i puntini.

  3. 3

    Errore 2: overflow verticale e ListView dentro Column

    Il secondo overflow più frequente è verticale, e nasce da due situazioni.

    a) Contenuto più alto dello schermo (tipico quando si apre la tastiera): la soluzione è rendere scrollabile la colonna con SingleChildScrollView.

    b) ListView dentro una Column: qui il messaggio è diverso, Vertical viewport was given unbounded height. La Column dice al figlio "prendi l'altezza che vuoi", ma la ListView risponde "dimmi tu quanta ne ho". Nessuno decide → errore.

    Le soluzioni:

    • Avvolgi la ListView in Expanded se deve occupare lo spazio rimanente e scorrere (soluzione preferita).
    • Usa shrinkWrap: true + physics: NeverScrollableScrollPhysics() solo se la lista è corta e deve scorrere insieme al resto della pagina: shrinkWrap costruisce tutti gli elementi, quindi è costoso su liste lunghe.

    Prova prima la versione sbagliata (togliendo Expanded) per vedere l'errore, poi applica il codice corretto.

    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        final items = List.generate(50, (i) => 'Elemento ${i + 1}');
    
        return Scaffold(
          appBar: AppBar(title: const Text('Overflow verticale')),
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              const Padding(
                padding: EdgeInsets.all(16),
                child: Text('Intestazione fissa',
                    style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
              ),
              // Expanded dà alla ListView un'altezza definita: niente unbounded height
              Expanded(
                child: ListView.builder(
                  itemCount: items.length,
                  itemBuilder: (context, index) => ListTile(
                    leading: const Icon(Icons.chevron_right),
                    title: Text(items[index]),
                  ),
                ),
              ),
            ],
          ),
        );
      }
    }

    Risultato atteso

    L'intestazione resta fissa in alto e la lista di 50 elementi scorre correttamente, senza errori in console.

  4. 4

    Errore 3: setState fuori posto (durante build, dopo dispose, in initState)

    setState() significa: "i dati sono cambiati, ricostruisci questo widget". Va chiamato solo in risposta a un evento (tap, timer, risposta di rete) e solo finché il widget è vivo.

    I tre casi patologici:

    1. setState() called during build — hai chiamato setState dentro build() (es. onPressed: _incrementa() invece di onPressed: _incrementa, oppure aggiornando lo stato mentre costruisci la UI). Si crea un loop infinito. Soluzione: passa il riferimento alla funzione, oppure usa WidgetsBinding.instance.addPostFrameCallback se devi davvero aggiornare dopo il primo frame.
    2. setState() called after dispose() — un Future o un Timer termina quando l'utente ha già lasciato la schermata. Soluzione: controlla if (!mounted) return; prima di chiamare setState, e cancella timer/subscription in dispose().
    3. setState con operazioni pesanti dentro — mettici solo l'assegnazione delle variabili, il lavoro (chiamate HTTP, calcoli) va fatto fuori, prima.

    ⚠️ Ricorda anche: modificare una variabile senza setState non aggiorna la UI; e setState in uno StatelessWidget non esiste (serve StatefulWidget).

    class ContatoreDemo extends StatefulWidget {
      const ContatoreDemo({super.key});
    
      @override
      State<ContatoreDemo> createState() => _ContatoreDemoState();
    }
    
    class _ContatoreDemoState extends State<ContatoreDemo> {
      int _contatore = 0;
      bool _caricamento = false;
    
      Future<void> _caricaDati() async {
        setState(() => _caricamento = true);
    
        // Lavoro pesante FUORI da setState
        await Future.delayed(const Duration(seconds: 2));
        final risultato = _contatore + 10;
    
        // Il widget potrebbe essere stato smontato durante l'attesa
        if (!mounted) return;
    
        setState(() {
          _contatore = risultato;
          _caricamento = false;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('setState corretto')),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Valore: $_contatore', style: const TextStyle(fontSize: 24)),
                const SizedBox(height: 16),
                _caricamento
                    ? const CircularProgressIndicator()
                    // NOTA: _caricaDati SENZA parentesi -> passiamo la funzione,
                    // non il suo risultato. Con le parentesi partirebbe durante build.
                    : ElevatedButton(
                        onPressed: _caricaDati,
                        child: const Text('Carica'),
                      ),
              ],
            ),
          ),
        );
      }
    }

    Risultato atteso

    Premendo "Carica" appare lo spinner per 2 secondi, poi il valore aumenta di 10. Uscendo dalla schermata durante il caricamento non compare più l'errore "setState() called after dispose()".

  5. 5

    Errore 4: null safety — `?`, `!`, `late` e i loro tranelli

    In Dart una variabile non può essere null a meno che il tipo non finisca con ?. Gli errori più comuni per chi inizia:

    • The argument type 'String?' can't be assigned to the parameter type 'String': stai passando un valore potenzialmente nullo dove ne serve uno sicuro. Risolvi con un valore di default (??) o con un controllo esplicito.
    • Null check operator used on a null value: hai usato ! ("fidati, non è null") su qualcosa che era null. Il ! non è una correzione: è una promessa che stai facendo al compilatore.
    • LateInitializationError: Field 'x' has not been initialized: hai dichiarato late una variabile e l'hai letta prima di assegnarla. late è utile in initState, non come scorciatoia per zittire il compilatore.

    Regole pratiche:

    1. Usa ?. per accedere in sicurezza e ?? per un valore di riserva.
    2. Usa il type promotion: if (valore != null) { ... } su una variabile locale rende il tipo non nullabile all'interno del blocco.
    3. Per i campi di classe nullabili, copiali prima in una variabile locale (final v = campo;) per abilitare la promozione.
    4. Riserva ! ai casi in cui hai davvero una garanzia (es. snapshot.data! dentro if (snapshot.hasData)).
    class Utente {
      final String nome;
      final String? email; // può mancare
    
      Utente({required this.nome, this.email});
    }
    
    String descriviUtente(Utente? utente) {
      // 1. Accesso sicuro + valore di default
      final nome = utente?.nome ?? 'Ospite';
    
      // 2. Type promotion su variabile locale
      final email = utente?.email;
      if (email != null && email.isNotEmpty) {
        return '$nome <$email>'; // qui email è String, non String?
      }
    
      return '$nome (email non disponibile)';
    }
    
    // Uso corretto di late: assegnato in initState, letto solo dopo
    class _EsempioLateState extends State<EsempioLate> {
      late final TextEditingController _controller;
    
      @override
      void initState() {
        super.initState();
        _controller = TextEditingController(text: 'valore iniziale');
      }
    
      @override
      void dispose() {
        _controller.dispose(); // sempre! altrimenti memory leak
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return TextField(controller: _controller);
      }
    }

    Risultato atteso

    `descriviUtente(null)` restituisce "Ospite (email non disponibile)" senza lanciare eccezioni, e il TextField funziona senza LateInitializationError.

  6. 6

    Errore 5: usare il BuildContext dopo un await

    Molto comune quando si mescolano async e navigazione. Dopo un await, il widget potrebbe non essere più nell'albero: usare quel context per Navigator, ScaffoldMessenger o showDialog provoca eccezioni o warning dell'analyzer (use_build_context_synchronously).

    La soluzione moderna (Flutter 3.7+) è il controllo context.mounted; in alternativa, in uno State, il classico if (!mounted) return;.

    Un'altra buona pratica: catturare gli oggetti che dipendono dal context prima dell'await, ad esempio final messenger = ScaffoldMessenger.of(context);.

    Future<void> _salvaEChiudi(BuildContext context) async {
      // Catturiamo prima dell'await ciò che dipende dal context
      final messenger = ScaffoldMessenger.of(context);
      final navigator = Navigator.of(context);
    
      await Future.delayed(const Duration(seconds: 1)); // simula il salvataggio
    
      // Controllo esplicito prima di usare di nuovo il context
      if (!context.mounted) return;
    
      messenger.showSnackBar(
        const SnackBar(content: Text('Salvataggio completato')),
      );
      navigator.pop();
    }

    Risultato atteso

    Nessun warning dell'analyzer e nessuna eccezione se l'utente chiude la schermata durante l'operazione asincrona.

  7. 7

    Checklist finale e strumenti per non ricadere negli stessi errori

    Prima di chiedere aiuto su un errore, passa questa checklist:

    Overflow

    • [ ] Il figlio problematico è dentro Row/Column? → prova Expanded o Flexible.
    • [ ] Il contenuto può superare lo schermo (tastiera, testi lunghi, tablet)? → SingleChildScrollView.
    • [ ] ListView dentro Column? → Expanded (o shrinkWrap: true solo per liste corte).
    • [ ] Testo lungo in una Row? → Expanded + overflow: TextOverflow.ellipsis.

    setState

    • [ ] Lo chiamo solo da callback/eventi, mai dentro build.
    • [ ] onPressed: _funzione (senza parentesi).
    • [ ] if (!mounted) return; dopo ogni await.
    • [ ] dispose() cancella controller, timer e subscription.

    Null safety

    • [ ] Ho evitato ! a meno di avere una garanzia reale.
    • [ ] Uso ?? e ?. per i valori opzionali.
    • [ ] late solo quando l'inizializzazione avviene sicuramente prima dell'uso.

    Strumenti da attivare subito

    • flutter analyze prima di ogni commit.
    • Il pacchetto flutter_lints (già incluso nei progetti nuovi) tramite analysis_options.yaml.
    • Flutter DevTools → Widget Inspector, in particolare il pulsante Debug Paint, che disegna i bordi dei layout e rende evidente chi sta sforando.

    Aggiungi queste regole al tuo analysis_options.yaml per farti avvisare in anticipo.

    # analysis_options.yaml
    include: package:flutter_lints/flutter.yaml
    
    linter:
      rules:
        - use_build_context_synchronously
        - prefer_const_constructors
        - avoid_print
        - cancel_subscriptions
        - close_sinks
    
    analyzer:
      errors:
        use_build_context_synchronously: error

    Risultato atteso

    Eseguendo `flutter analyze` l'editor segnala in anticipo l'uso scorretto del BuildContext e le altre cattive pratiche, prima ancora di lanciare l'app.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!