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à childCenter, Padding, Container, SizedBox;
    • widget con più figli usano childrenColumn, 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 + PFlutter: Open DevToolsOpen 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!