Anatomia di un'app Flutter: MaterialApp, Scaffold e albero dei widget

Foto di Yang JunJie su Unsplash

GuidePrincipiante35 min Flutter 3.x

Anatomia di un'app Flutter: MaterialApp, Scaffold e albero dei widget

Quando crei un nuovo progetto con flutter create, ti ritrovi davanti a un file main.dart pieno di classi e parentesi che, all'inizio, sembrano magia nera.

In realtà ogni app Flutter segue sempre la stessa struttura: una funzione main() che chiama runApp(), un widget radice (di solito MaterialApp) e, dentro, una o più schermate costruite con Scaffold.

In questo tutorial smontiamo pezzo per pezzo questa struttura. Alla fine saprai:

  • cosa fa esattamente runApp() e cos'è l'albero dei widget;
  • a cosa serve MaterialApp e quali proprietà usare subito (title, theme, home);
  • come si compone uno Scaffold (appBar, body, floatingActionButton, bottomNavigationBar);
  • come leggere un errore di layout capendo dove si trova il widget nell'albero.

Non serve alcuna esperienza pregressa: basta avere Flutter installato e un progetto vuoto pronto.

  1. 1

    Creare il progetto e svuotare main.dart

    Partiamo da zero, senza il contatore generato da Flutter, così vediamo la struttura minima di un'app.

    Apri il terminale e crea il progetto:

    flutter create anatomia_app
    cd anatomia_app
    

    Apri lib/main.dart e cancella tutto il contenuto, sostituendolo con il codice qui sotto.

    Cosa succede in queste poche righe:

    • import 'package:flutter/material.dart'; porta dentro tutti i widget del Material Design (bottoni, AppBar, temi…).
    • void main() è il punto di ingresso di ogni programma Dart, quindi anche della tua app.
    • runApp() prende un widget e lo attacca alla radice dell'albero, dicendo a Flutter: «questa è l'interfaccia, disegnala sullo schermo».

    Nota che stiamo passando direttamente un Center con del testo: runApp() accetta qualsiasi widget, non per forza un MaterialApp. Il Directionality serve perché senza MaterialApp nessuno ha ancora deciso la direzione del testo (da sinistra a destra).

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(
        const Directionality(
          textDirection: TextDirection.ltr,
          child: Center(
            child: Text('Ciao Flutter!'),
          ),
        ),
      );
    }

    Risultato atteso

    Lanciando `flutter run` vedi una schermata nera (o bianca, dipende dal device) con la scritta "Ciao Flutter!" al centro, senza barra in alto e con un testo dall'aspetto grezzo e sottolineato di giallo.

  2. 2

    Aggiungere MaterialApp: il widget radice dell'app

    L'esempio precedente funziona ma è inutilizzabile: niente tema, niente navigazione, niente font di default sensati.

    MaterialApp è il widget che configura l'intera applicazione. Sotto il cofano fornisce a tutti i widget figli:

    • un Theme (colori, tipografia, forme);
    • un Navigator per passare da una schermata all'altra;
    • la direzione del testo e le localizzazioni di base;
    • il titolo che il sistema operativo mostra nel task manager (title).

    Le proprietà che userai subito sono tre:

    Proprietà A cosa serve
    title Nome dell'app usato dal sistema (non è visibile nella UI)
    theme Colori e stili globali
    home Il widget della prima schermata

    debugShowCheckedModeBanner: false rimuove il nastro rosso "DEBUG" in alto a destra.

    Creiamo anche la nostra prima classe widget: MyApp, che estende StatelessWidget e restituisce il MaterialApp dal metodo build(). Scrivere widget come classi è la norma in Flutter: rende il codice riutilizzabile e leggibile.

    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: 'Anatomia App',
          debugShowCheckedModeBanner: false,
          theme: ThemeData(
            colorSchemeSeed: Colors.indigo,
            useMaterial3: true,
          ),
          home: const HomePage(),
        );
      }
    }
    
    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const Center(child: Text('Ciao Flutter!'));
      }
    }

    Risultato atteso

    L'app mostra un fondo bianco (o secondo il tema di sistema) con il testo centrato, leggibile e senza sottolineatura gialla. Il banner DEBUG è sparito.

  3. 3

    Costruire la schermata con Scaffold: appBar, body e FAB

    MaterialApp configura l'app, ma non disegna una schermata. Il widget che dà la struttura visiva a una pagina è Scaffold (in italiano: "impalcatura").

    Uno Scaffold mette a disposizione degli slot già posizionati correttamente:

    • appBar: la barra superiore (AppBar con title, actions, pulsante indietro automatico);
    • body: il contenuto principale, che occupa lo spazio rimanente;
    • floatingActionButton: il bottone circolare in basso a destra;
    • bottomNavigationBar: la barra di navigazione inferiore;
    • drawer: il menu laterale a scomparsa.

    La regola pratica da ricordare: una schermata = uno Scaffold. Se una pagina non ha lo Scaffold, ti mancheranno la gestione della SafeArea, il colore di sfondo del tema e la posizione automatica di SnackBar e FAB.

    Riscriviamo HomePage usando lo Scaffold.

    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Anatomia di un\'app'),
            actions: [
              IconButton(
                icon: const Icon(Icons.info_outline),
                onPressed: () {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('Questa è la AppBar!')),
                  );
                },
              ),
            ],
          ),
          body: const Center(
            child: Text('Contenuto della pagina'),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {},
            child: const Icon(Icons.add),
          ),
        );
      }
    }

    Risultato atteso

    Vedi una barra in alto colorata col titolo "Anatomia di un'app" e un'icona info a destra, il testo al centro dello schermo e un bottone circolare con il "+" in basso a destra. Toccando l'icona info compare una SnackBar.

  4. 4

    Riempire il body: l'albero dei widget in pratica

    Ora arriviamo al concetto chiave: in Flutter tutto è un widget e i widget si annidano formando un albero.

    Ogni widget ha un genitore e (spesso) dei figli:

    • widget con un solo figlio usano la proprietà child → Center, Padding, Container, SizedBox;
    • widget con più figli usano children → Column, Row, ListView, Stack.

    Quando scrivi il body stai in realtà descrivendo un ramo dell'albero. Ecco l'albero del codice qui sotto, letto dall'alto verso il basso:

    Scaffold
     └─ body: Padding
         └─ Column
             ├─ CircleAvatar
             ├─ SizedBox
             ├─ Text (nome)
             ├─ Text (ruolo)
             ├─ SizedBox
             └─ FilledButton
    

    Un consiglio di lettura: l'indentazione del codice è l'albero. Se ti perdi tra le parentesi, in VS Code o Android Studio usa Ctrl/Cmd + . sul widget e scegli Wrap with... per annidare senza sbagliare.

    body: Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          const CircleAvatar(
            radius: 48,
            child: Icon(Icons.person, size: 48),
          ),
          const SizedBox(height: 16),
          Text(
            'Giulia Rossi',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          Text(
            'Flutter Developer',
            style: Theme.of(context).textTheme.bodyMedium,
          ),
          const SizedBox(height: 24),
          FilledButton.icon(
            onPressed: () {},
            icon: const Icon(Icons.mail_outline),
            label: const Text('Contatta'),
          ),
        ],
      ),
    ),

    Risultato atteso

    Il body mostra una scheda profilo verticale: avatar circolare, nome in grande, ruolo sotto e un bottone "Contatta", il tutto centrato con margini di 24 pixel.

  5. 5

    Esplorare l'albero con i DevTools e il Widget Inspector

    Finora l'albero l'abbiamo immaginato leggendo il codice. Flutter però ti permette di vederlo davvero.

    Con l'app in esecuzione (flutter run o tasto Run dell'IDE):

    1. In VS Code: Ctrl/Cmd + Shift + P → Flutter: Open DevTools → Open Widget Inspector.
    2. In Android Studio: tab Flutter Inspector nella barra laterale destra.
    3. Da terminale: dopo flutter run premi v per aprire DevTools nel browser.

    Nel Widget Inspector trovi:

    • l'albero completo dei widget, dalla radice MaterialApp fino ai Text;
    • il pulsante Select Widget Mode: attivalo, tocca un elemento sullo schermo e l'inspector evidenzierà il widget corrispondente nell'albero;
    • il pannello Layout Explorer, utilissimo per capire come Column e Row distribuiscono lo spazio;
    • il toggle Show Guidelines (Debug Paint) che disegna i bordi di ogni box.

    Come esercizio, aggiungi temporaneamente il codice qui sotto nel build per stampare l'albero in console e confrontarlo con quello dell'inspector.

    Suggerimento: ricordati di ricorrere all'hot reload (r nel terminale o salvataggio del file) per vedere le modifiche in meno di un secondo, senza riavviare l'app.

    @override
    Widget build(BuildContext context) {
      // Solo per studio: stampa il ramo dell'albero da questo widget in su.
      debugPrint(context.toString());
      debugPrint('Widget genitore Scaffold: '
          '${context.findAncestorWidgetOfExactType<MaterialApp>()}');
    
      return Scaffold(
        // ... resto del codice
      );
    }

    Risultato atteso

    Nella console vedi la descrizione del BuildContext corrente e il riferimento al MaterialApp genitore; nel Widget Inspector puoi navigare l'albero e, selezionando l'avatar sullo schermo, vederlo evidenziato nella gerarchia.

  6. 6

    Separare i widget in classi: albero pulito e riutilizzabile

    Man mano che la schermata cresce, il metodo build diventa una piramide di parentesi. La soluzione idiomatica in Flutter è estrarre porzioni dell'albero in widget dedicati.

    Vantaggi concreti:

    • il codice è più leggibile e ogni pezzo ha un nome parlante;
    • i widget estratti possono essere marcati const e Flutter li salta durante la ricostruzione, migliorando le performance;
    • puoi riutilizzare ProfileHeader in altre schermate.

    Una scorciatoia dell'IDE ti evita il lavoro manuale: posiziona il cursore sul widget, premi Ctrl/Cmd + . e scegli Extract Widget (evita Extract Method: restituire widget da un metodo non permette a Flutter di ottimizzarne la ricostruzione).

    Ecco come diventa il file finale, con l'albero suddiviso in classi.

    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: 'Anatomia App',
          debugShowCheckedModeBanner: false,
          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('Anatomia di un\'app')),
          body: const Padding(
            padding: EdgeInsets.all(24),
            child: ProfileHeader(
              nome: 'Giulia Rossi',
              ruolo: 'Flutter Developer',
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {},
            child: const Icon(Icons.add),
          ),
        );
      }
    }
    
    class ProfileHeader extends StatelessWidget {
      const ProfileHeader({super.key, required this.nome, required this.ruolo});
    
      final String nome;
      final String ruolo;
    
      @override
      Widget build(BuildContext context) {
        final testi = Theme.of(context).textTheme;
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const CircleAvatar(radius: 48, child: Icon(Icons.person, size: 48)),
            const SizedBox(height: 16),
            Text(nome, style: testi.headlineSmall),
            Text(ruolo, style: testi.bodyMedium),
          ],
        );
      }
    }

    Risultato atteso

    L'app appare identica a prima, ma il codice è diviso in tre widget chiari (MyApp, HomePage, ProfileHeader) e nel Widget Inspector compare il nodo ProfileHeader come blocco a sé.

  7. 7

    Errori tipici e checklist finale

    Prima di chiudere, ecco i tre inciampi più frequenti quando si costruisce l'albero di un'app, con la soluzione.

    1. Scaffold.of() called with a context that does not contain a Scaffold
    Succede quando chiami ScaffoldMessenger.of(context) usando il context del build che crea lo Scaffold. Soluzione: estrai il figlio in un widget separato (come ProfileHeader) oppure usa un Builder.

    2. A RenderFlex overflowed by N pixels
    La Column contiene più contenuto dell'altezza disponibile. Soluzione: avvolgi la Column in un SingleChildScrollView, oppure usa Expanded/Flexible sul figlio che deve adattarsi.

    3. Dimenticare home o restituire una schermata senza Scaffold
    Il risultato è testo giallo sottolineato su fondo nero: significa che manca il Material che fornisce tema e sfondo.

    Checklist della struttura corretta

    • [ ] main() chiama runApp() una sola volta
    • [ ] il widget radice è un MaterialApp (o CupertinoApp)
    • [ ] theme definito una volta sola, non stili ripetuti nei singoli widget
    • [ ] ogni schermata ha il proprio Scaffold
    • [ ] i costruttori dei widget hanno const dove possibile
    • [ ] i rami complessi sono estratti in classi con nomi parlanti

    Da qui il passo successivo naturale è la navigazione: sostituire il contenuto di home e spostarti tra più Scaffold con Navigator.push e Navigator.pop.

    // Esempio di fix per l'overflow: contenuto scrollabile
    body: SingleChildScrollView(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: const [
          ProfileHeader(nome: 'Giulia Rossi', ruolo: 'Flutter Developer'),
          SizedBox(height: 400),
          Text('Contenuto molto lungo...'),
        ],
      ),
    ),

    Risultato atteso

    Anche con contenuti più alti dello schermo non compare più la striscia gialla e nera di overflow: la pagina diventa scorrevole.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!