Spaziature e scroll in Flutter: SafeArea, SizedBox, Spacer e SingleChildScrollView

Foto di Mediamodifier su Unsplash

GuidePrincipiante30 min Flutter 3.x

Spaziature e scroll in Flutter: SafeArea, SizedBox, Spacer e SingleChildScrollView

Quando si costruisce la prima interfaccia in Flutter, il problema non è quasi mai "come mostro un testo", ma come lo posiziono bene: elementi troppo attaccati, contenuti nascosti sotto il notch, il famigerato nastro giallo e nero di overflow appena si apre la tastiera.

In questo tutorial costruiamo passo passo una schermata di profilo e impariamo a usare quattro widget semplicissimi ma fondamentali:

  • SafeArea per non finire sotto notch, barra di stato e home indicator;
  • SizedBox per creare spazi fissi e prevedibili;
  • Spacer (ed Expanded) per distribuire lo spazio rimasto;
  • SingleChildScrollView per rendere scorrevole una schermata che non ci sta nello schermo.

Non serve nessun pacchetto esterno: tutto quello che useremo fa parte di Flutter. Al termine avrai una schermata pulita, che si comporta bene su telefoni piccoli e grandi e che non va in overflow quando l'utente scrive in un campo di testo.

  1. 1

    Creare la schermata di partenza

    Partiamo da un progetto Flutter vuoto (flutter create spazi_demo) e sostituiamo il contenuto di lib/main.dart.

    Creiamo una semplice schermata di profilo con un avatar, un nome e una breve descrizione, dentro una Column. Per ora non aggiungiamo spaziature: vogliamo vedere il problema prima di risolverlo.

    Nota la proprietà crossAxisAlignment: CrossAxisAlignment.stretch: fa sì che i figli della Column occupino tutta la larghezza disponibile, utile per i bottoni.

    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: 'Spazi Demo',
          theme: ThemeData(
            colorSchemeSeed: Colors.indigo,
            useMaterial3: true,
          ),
          home: const ProfiloPage(),
        );
      }
    }
    
    class ProfiloPage extends StatelessWidget {
      const ProfiloPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: const [
              CircleAvatar(
                radius: 48,
                child: Icon(Icons.person, size: 48),
              ),
              Text(
                'Giulia Rossi',
                textAlign: TextAlign.center,
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
              ),
              Text(
                'Flutter developer a Milano',
                textAlign: TextAlign.center,
              ),
            ],
          ),
        );
      }
    }

    Risultato atteso

    L'app si avvia e mostra avatar, nome e descrizione tutti attaccati l'uno all'altro, con il contenuto che parte dal bordo superiore dello schermo (in parte sotto la barra di stato).

  2. 2

    Proteggere il contenuto con SafeArea

    Il primo problema è evidente: il contenuto finisce sotto la barra di stato (e sui telefoni con notch può essere coperto).

    SafeArea è un widget che aggiunge automaticamente il padding necessario per evitare le zone "non sicure" dello schermo: barra di stato, notch, home indicator di iOS.

    Si usa semplicemente avvolgendo il contenuto:

    SafeArea(child: ...)
    

    Puoi anche disattivare singoli lati, ad esempio SafeArea(top: false, child: ...) se sopra hai già una AppBar (che gestisce da sola l'area sicura).

    Aggiungiamo anche un Padding orizzontale di 24 pixel per staccare il contenuto dai bordi laterali: è la regola d'oro di ogni layout leggibile.

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        body: SafeArea(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 24),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: const [
                CircleAvatar(
                  radius: 48,
                  child: Icon(Icons.person, size: 48),
                ),
                Text(
                  'Giulia Rossi',
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                ),
                Text(
                  'Flutter developer a Milano',
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ),
        ),
      );
    }

    Risultato atteso

    Il contenuto non è più sotto la barra di stato ed è staccato di 24 pixel dai bordi laterali. Gli elementi sono però ancora incollati verticalmente.

  3. 3

    SizedBox: spazi fissi e prevedibili

    Per separare verticalmente i widget di una Column il modo più idiomatico in Flutter è inserire dei SizedBox con un'altezza fissa.

    SizedBox è un widget che occupa semplicemente lo spazio che gli dici:

    • SizedBox(height: 16) → spazio verticale di 16 px;
    • SizedBox(width: 8) → spazio orizzontale di 8 px (dentro una Row);
    • SizedBox(height: 48, child: ElevatedButton(...)) → forza l'altezza di un figlio.

    Usa sempre const davanti a SizedBox: è un widget immutabile e Flutter può riutilizzarlo senza ricostruirlo.

    Suggerimento pratico: lavora con multipli di 4 o 8 (4, 8, 16, 24, 32). Mantiene il design coerente ed è la base dei sistemi di spaziatura di Material Design.

    Aggiungiamo anche una Row con due statistiche, separate da uno SizedBox orizzontale.

    Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        const SizedBox(height: 32),
        const CircleAvatar(
          radius: 48,
          child: Icon(Icons.person, size: 48),
        ),
        const SizedBox(height: 16),
        const Text(
          'Giulia Rossi',
          textAlign: TextAlign.center,
          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 8),
        const Text(
          'Flutter developer a Milano',
          textAlign: TextAlign.center,
        ),
        const SizedBox(height: 24),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const [
            _Statistica(valore: '128', etichetta: 'Progetti'),
            SizedBox(width: 32),
            _Statistica(valore: '2.4k', etichetta: 'Follower'),
          ],
        ),
      ],
    )

    Risultato atteso

    Gli elementi sono ora distanziati in modo regolare e leggibile. Devi ancora creare il widget `_Statistica`, che vedremo nel passo successivo.

  4. 4

    Creare il piccolo widget _Statistica

    Per tenere il codice ordinato estraiamo le statistiche in un widget privato (il prefisso _ lo rende visibile solo in questo file).

    Qui usiamo di nuovo SizedBox per separare il numero dall'etichetta e un Column con mainAxisSize: MainAxisSize.min, che dice alla colonna di occupare solo lo spazio necessario invece di espandersi in verticale. È un dettaglio importante: dentro una Row, una Column senza mainAxisSize.min tenta di prendere tutta l'altezza disponibile.

    Aggiungi questa classe in fondo a main.dart.

    class _Statistica extends StatelessWidget {
      const _Statistica({required this.valore, required this.etichetta});
    
      final String valore;
      final String etichetta;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              valore,
              style: const TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 4),
            Text(
              etichetta,
              style: TextStyle(color: Colors.grey.shade600),
            ),
          ],
        );
      }
    }

    Risultato atteso

    Sotto la descrizione compaiono due blocchi di statistiche affiancati e centrati, con numero in grassetto ed etichetta grigia.

  5. 5

    Spacer: distribuire lo spazio che avanza

    SizedBox crea spazi fissi. Ma se volessimo "spingere" un bottone in fondo allo schermo, qualunque sia l'altezza del dispositivo?

    Qui entra in gioco Spacer: è un widget che occupa tutto lo spazio libero rimasto lungo l'asse principale. In pratica è una scorciatoia per Expanded(child: SizedBox()).

    Regole da ricordare:

    • funziona solo dentro Row, Column o Flex;
    • se ci sono più Spacer, lo spazio si divide in base al parametro flex (default 1);
    • non si può usare dentro uno scroll (lo vedremo nel prossimo passo): lì lo spazio disponibile è infinito e Flutter solleva un errore.

    Aggiungiamo in fondo alla Column uno Spacer e un bottone.

    Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        const SizedBox(height: 32),
        // ... avatar, nome, descrizione, statistiche ...
        const Spacer(), // spinge il bottone verso il fondo
        FilledButton(
          onPressed: () {},
          child: const Padding(
            padding: EdgeInsets.symmetric(vertical: 12),
            child: Text('Modifica profilo'),
          ),
        ),
        const SizedBox(height: 16),
        OutlinedButton(
          onPressed: () {},
          child: const Padding(
            padding: EdgeInsets.symmetric(vertical: 12),
            child: Text('Condividi'),
          ),
        ),
        const SizedBox(height: 24),
      ],
    )

    Risultato atteso

    I due bottoni sono ancorati nella parte bassa dello schermo e occupano tutta la larghezza disponibile, mentre il profilo resta in alto.

  6. 6

    Evitare l'overflow con SingleChildScrollView

    Prova a ruotare il dispositivo in orizzontale o ad aprire l'app su uno schermo piccolo: comparirà il classico nastro giallo e nero con il messaggio A RenderFlex overflowed by X pixels.

    La soluzione è rendere la pagina scorrevole con SingleChildScrollView, che permette al suo unico figlio di essere più alto dello schermo.

    Attenzione però: dentro uno scroll non puoi usare Spacer o Expanded, perché l'altezza disponibile diventa illimitata. La tecnica standard per avere "scroll quando serve, bottoni in fondo quando c'è spazio" è combinare LayoutBuilder, ConstrainedBox e IntrinsicHeight.

    In alternativa, se vuoi la massima semplicità, sostituisci lo Spacer con un SizedBox(height: 32) e avvolgi tutto in SingleChildScrollView. Qui vediamo la versione completa e robusta.

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        body: SafeArea(
          child: LayoutBuilder(
            builder: (context, constraints) {
              return SingleChildScrollView(
                padding: const EdgeInsets.symmetric(horizontal: 24),
                child: ConstrainedBox(
                  constraints: BoxConstraints(
                    minHeight: constraints.maxHeight,
                  ),
                  child: IntrinsicHeight(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: [
                        const SizedBox(height: 32),
                        const CircleAvatar(
                          radius: 48,
                          child: Icon(Icons.person, size: 48),
                        ),
                        const SizedBox(height: 16),
                        const Text(
                          'Giulia Rossi',
                          textAlign: TextAlign.center,
                          style: TextStyle(
                              fontSize: 24, fontWeight: FontWeight.bold),
                        ),
                        const SizedBox(height: 8),
                        const Text(
                          'Flutter developer a Milano',
                          textAlign: TextAlign.center,
                        ),
                        const SizedBox(height: 24),
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: const [
                            _Statistica(valore: '128', etichetta: 'Progetti'),
                            SizedBox(width: 32),
                            _Statistica(valore: '2.4k', etichetta: 'Follower'),
                          ],
                        ),
                        const SizedBox(height: 24),
                        const TextField(
                          decoration: InputDecoration(
                            labelText: 'Bio',
                            border: OutlineInputBorder(),
                          ),
                        ),
                        const Spacer(),
                        FilledButton(
                          onPressed: () {},
                          child: const Padding(
                            padding: EdgeInsets.symmetric(vertical: 12),
                            child: Text('Modifica profilo'),
                          ),
                        ),
                        const SizedBox(height: 16),
                        OutlinedButton(
                          onPressed: () {},
                          child: const Padding(
                            padding: EdgeInsets.symmetric(vertical: 12),
                            child: Text('Condividi'),
                          ),
                        ),
                        const SizedBox(height: 24),
                      ],
                    ),
                  ),
                ),
              );
            },
          ),
        ),
      );
    }

    Risultato atteso

    Su schermi grandi i bottoni restano in basso grazie allo Spacer; su schermi piccoli o quando si apre la tastiera la pagina diventa scorrevole e il nastro di overflow sparisce.

  7. 7

    Rifinire con Container, Padding e margin

    Ultimo tassello: la differenza tra Padding e Container.

    • Padding aggiunge solo spazio interno attorno al figlio. È leggero e va preferito quando ti serve soltanto quello.
    • Container è un widget "coltellino svizzero": può avere padding, margin, color, decoration, dimensioni fisse. Usalo quando ti serve più del semplice spazio.

    Ricorda la distinzione: padding = spazio dentro il bordo, margin = spazio fuori dal bordo.

    Avvolgiamo la riga delle statistiche in un Container decorato per vedere la differenza all'opera: il padding allontana i numeri dal bordo grigio, il margin allontana il riquadro dagli altri widget.

    Container(
      margin: const EdgeInsets.symmetric(vertical: 8),
      padding: const EdgeInsets.symmetric(vertical: 16),
      decoration: BoxDecoration(
        color: Colors.indigo.withOpacity(0.06),
        borderRadius: BorderRadius.circular(16),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: const [
          _Statistica(valore: '128', etichetta: 'Progetti'),
          _Statistica(valore: '2.4k', etichetta: 'Follower'),
          _Statistica(valore: '87', etichetta: 'Seguiti'),
        ],
      ),
    )

    Risultato atteso

    Le statistiche appaiono dentro un riquadro con sfondo tenue e angoli arrotondati, distanziate automaticamente grazie a MainAxisAlignment.spaceEvenly e staccate dagli altri elementi dal margin.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!