Costruire una card profilo in Flutter: Row, Column, Expanded e Padding in pratica

Foto di shahriyar khalid su Unsplash

GuidePrincipiante35 min Flutter 3.x

Costruire una card profilo in Flutter: Row, Column, Expanded e Padding in pratica

Quando si inizia con Flutter la domanda più frequente è sempre la stessa: come dispongo i widget sullo schermo?

La risposta sta in pochi widget di layout che userai ogni giorno: Container, Row, Column, Expanded, Padding e SizedBox. Invece di studiarli in astratto, in questo tutorial li metteremo subito al lavoro costruendo un componente reale: una card profilo con avatar, nome, biografia, statistiche e pulsanti di azione.

Alla fine avrai:

  • capito la differenza fra asse principale e asse trasversale;
  • imparato quando usare Expanded e quando Flexible;
  • visto da dove nascono i famigerati errori di overflow gialli e neri;
  • ottenuto un widget riutilizzabile da incollare nei tuoi progetti.

Serve solo Flutter installato e un progetto vuoto creato con flutter create. Non useremo nessun pacchetto esterno.

  1. 1

    Preparare il progetto e una schermata pulita

    Crea un nuovo progetto e svuota lib/main.dart dal contatore di default. Partiamo da uno Scaffold con uno sfondo grigio chiaro e un Center: sarà la nostra tela bianca.

    Nota alcune cose importanti già in questo scheletro:

    • MaterialApp fornisce temi, routing e direzione del testo: senza di lui molti widget non funzionano;
    • Scaffold dà la struttura base della pagina (AppBar, body, sfondo);
    • Padding con EdgeInsets.all(16) crea un margine uniforme su tutti i lati.

    Sostituisci tutto il contenuto di lib/main.dart con il codice qui sotto e avvia l'app con flutter run.

    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: 'Layout Flutter',
          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(
          backgroundColor: const Color(0xFFF2F3F7),
          appBar: AppBar(title: const Text('Card profilo')),
          body: const Center(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Text('Qui costruiremo la card'),
            ),
          ),
        );
      }
    }

    Risultato atteso

    L'app si avvia mostrando una AppBar con il titolo "Card profilo" e un testo centrato su sfondo grigio chiaro.

  2. 2

    Il contenitore: Container, BoxDecoration e Padding

    Il Container è il mattone di base: può avere dimensioni, colore, bordi, ombre, margini e padding.

    Attenzione a una regola che confonde molti principianti: non puoi usare insieme color e decoration. Se ti serve un bordo arrotondato o un'ombra, metti il colore dentro BoxDecoration.

    Differenza fondamentale:

    • margin → spazio fuori dal container;
    • padding → spazio dentro il container, tra il bordo e il figlio.

    Creiamo un nuovo widget ProfileCard e inseriamolo nella home al posto del testo.

    class ProfileCard extends StatelessWidget {
      const ProfileCard({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Container(
          padding: const EdgeInsets.all(20),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(20),
            boxShadow: const [
              BoxShadow(
                color: Color(0x14000000),
                blurRadius: 16,
                offset: Offset(0, 6),
              ),
            ],
          ),
          child: const Text('Contenuto della card'),
        );
      }
    }
    
    // In HomePage sostituisci il body con:
    // body: const Center(
    //   child: Padding(
    //     padding: EdgeInsets.all(16),
    //     child: ProfileCard(),
    //   ),
    // ),

    Risultato atteso

    Al centro dello schermo compare un riquadro bianco con angoli arrotondati e una leggera ombra.

  3. 3

    Row e Column: avatar a sinistra, testi a destra

    Ora riempiamo la card. La struttura logica è:

    • una Row (disposizione orizzontale) che contiene avatar e blocco testi;
    • dentro la Row, una Column (disposizione verticale) con nome e ruolo.

    Due proprietà da memorizzare:

    • mainAxisAlignment → allinea lungo l'asse principale (orizzontale per Row, verticale per Column);
    • crossAxisAlignment → allinea lungo l'asse opposto.

    Di default una Column occupa tutta l'altezza disponibile: con mainAxisSize: MainAxisSize.min le diciamo di essere alta quanto i suoi figli. È proprio ciò che serve dentro una card.

    Per la spaziatura tra avatar e testi usiamo un SizedBox(width: 16): più leggero e più leggibile di un Padding.

    Row(
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        const CircleAvatar(
          radius: 32,
          backgroundColor: Color(0xFFE3E5F0),
          child: Icon(Icons.person, size: 34, color: Colors.indigo),
        ),
        const SizedBox(width: 16),
        Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: const [
            Text(
              'Giulia Ferrari',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 4),
            Text(
              'Mobile Developer a Milano',
              style: TextStyle(color: Colors.black54),
            ),
          ],
        ),
      ],
    )

    Risultato atteso

    Nella card appaiono un avatar circolare a sinistra e, a destra, nome e ruolo incolonnati.

  4. 4

    Expanded: risolvere l'overflow dei testi lunghi

    Prova a sostituire il ruolo con un testo molto lungo, ad esempio "Mobile Developer specializzata in Flutter e architetture a feature". Comparirà la famosa striscia gialla e nera: RenderFlex overflowed by ... pixels.

    Perché? Una Row concede ai figli tutta la larghezza che chiedono, senza limitarla. Il Text chiede più spazio di quello disponibile e sfora.

    La soluzione è Expanded: dice al figlio "occupa esattamente lo spazio rimanente, non di più". Con il vincolo di larghezza, il Text può andare a capo oppure essere troncato con overflow: TextOverflow.ellipsis.

    Regole pratiche:

    • Expanded → il figlio riempie lo spazio residuo;
    • Flexible → il figlio può occupare al massimo lo spazio residuo, ma resta più piccolo se gli basta;
    • flex → distribuisce lo spazio in proporzione tra più figli espansi.

    Expanded e Flexible funzionano solo dentro Row, Column o Flex.

    Row(
      children: [
        const CircleAvatar(
          radius: 32,
          backgroundColor: Color(0xFFE3E5F0),
          child: Icon(Icons.person, size: 34, color: Colors.indigo),
        ),
        const SizedBox(width: 16),
        Expanded( // <-- la Column occupa lo spazio residuo
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: const [
              Text(
                'Giulia Ferrari',
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
              ),
              SizedBox(height: 4),
              Text(
                'Mobile Developer specializzata in Flutter e architetture a feature',
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
                style: TextStyle(color: Colors.black54),
              ),
            ],
          ),
        ),
        const Icon(Icons.more_vert, color: Colors.black45),
      ],
    )

    Risultato atteso

    La striscia di overflow sparisce: il testo lungo va a capo su due righe e si tronca con i puntini, mentre l'icona resta allineata a destra.

  5. 5

    Una riga di statistiche con Expanded e flex uguali

    Aggiungiamo sotto l'intestazione una riga con tre statistiche (Post, Follower, Seguiti) larghe esattamente uguali.

    Il trucco è avvolgere ogni colonna in un Expanded: avendo tutti flex: 1 (valore di default), lo spazio viene diviso in parti uguali indipendentemente dalla lunghezza del testo.

    Per separare l'intestazione dalle statistiche usiamo un Divider, e per lo spazio verticale dei SizedBox(height: ...). Nota il piccolo widget privato _StatItem: estrarre pezzi ripetuti in widget separati rende il codice molto più leggibile (ed è una buona pratica Flutter).

    class _StatItem extends StatelessWidget {
      const _StatItem({required this.value, required this.label});
    
      final String value;
      final String label;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            Text(
              value,
              style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 2),
            Text(label, style: const TextStyle(color: Colors.black54, fontSize: 13)),
          ],
        );
      }
    }
    
    // Da inserire nella Column principale della card:
    const Divider(height: 32),
    Row(
      children: const [
        Expanded(child: _StatItem(value: '128', label: 'Post')),
        Expanded(child: _StatItem(value: '4.2k', label: 'Follower')),
        Expanded(child: _StatItem(value: '310', label: 'Seguiti')),
      ],
    ),

    Risultato atteso

    Sotto l'intestazione compare una riga con tre blocchi di statistiche perfettamente equidistanti e centrati.

  6. 6

    I pulsanti: Expanded con flex diversi

    Chiudiamo la card con due pulsanti: "Segui" più largo e "Messaggio" più stretto.

    Qui flex mostra tutta la sua utilità: con flex: 2 e flex: 1 lo spazio disponibile viene diviso in 3 parti, 2 al primo pulsante e 1 al secondo.

    Un dettaglio utile: per dare un'altezza fissa ai bottoni possiamo avvolgerli in un SizedBox(height: 48) oppure usare minimumSize nello stile. Qui scegliamo la seconda via, così il codice resta compatto.

    const SizedBox(height: 20),
    Row(
      children: [
        Expanded(
          flex: 2,
          child: ElevatedButton.icon(
            onPressed: () {},
            icon: const Icon(Icons.person_add_alt),
            label: const Text('Segui'),
            style: ElevatedButton.styleFrom(
              minimumSize: const Size.fromHeight(48),
            ),
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          flex: 1,
          child: OutlinedButton(
            onPressed: () {},
            style: OutlinedButton.styleFrom(
              minimumSize: const Size.fromHeight(48),
            ),
            child: const Text('Messaggio'),
          ),
        ),
      ],
    ),

    Risultato atteso

    In fondo alla card compaiono due pulsanti affiancati: quello "Segui" occupa il doppio dello spazio rispetto a "Messaggio".

  7. 7

    Mettere tutto insieme e rendere la card riutilizzabile

    Ultimo passo: uniamo i pezzi in un unico widget ProfileCard che riceve i dati dall'esterno tramite parametri. Così potrai usarlo in qualunque schermata, anche dentro una ListView.

    Alcune rifiniture finali:

    • SafeArea evita che il contenuto finisca sotto la notch o la barra di sistema;
    • SingleChildScrollView previene l'overflow verticale quando la tastiera si apre o lo schermo è piccolo;
    • ConstrainedBox con maxWidth evita che la card diventi larghissima su tablet e desktop;
    • const davanti ai widget statici aiuta le performance.

    Prova a cambiare crossAxisAlignment, i valori di flex e il padding: è il modo migliore per interiorizzare come ragiona il motore di layout di Flutter.

    import 'package:flutter/material.dart';
    
    class ProfileCard extends StatelessWidget {
      const ProfileCard({
        super.key,
        required this.name,
        required this.role,
        required this.posts,
        required this.followers,
        required this.following,
      });
    
      final String name;
      final String role;
      final String posts;
      final String followers;
      final String following;
    
      @override
      Widget build(BuildContext context) {
        return Container(
          padding: const EdgeInsets.all(20),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(20),
            boxShadow: const [
              BoxShadow(color: Color(0x14000000), blurRadius: 16, offset: Offset(0, 6)),
            ],
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Row(
                children: [
                  const CircleAvatar(
                    radius: 32,
                    backgroundColor: Color(0xFFE3E5F0),
                    child: Icon(Icons.person, size: 34, color: Colors.indigo),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          name,
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                          style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                        ),
                        const SizedBox(height: 4),
                        Text(
                          role,
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                          style: const TextStyle(color: Colors.black54),
                        ),
                      ],
                    ),
                  ),
                  const Icon(Icons.more_vert, color: Colors.black45),
                ],
              ),
              const Divider(height: 32),
              Row(
                children: [
                  Expanded(child: _StatItem(value: posts, label: 'Post')),
                  Expanded(child: _StatItem(value: followers, label: 'Follower')),
                  Expanded(child: _StatItem(value: following, label: 'Seguiti')),
                ],
              ),
              const SizedBox(height: 20),
              Row(
                children: [
                  Expanded(
                    flex: 2,
                    child: ElevatedButton.icon(
                      onPressed: () {},
                      icon: const Icon(Icons.person_add_alt),
                      label: const Text('Segui'),
                      style: ElevatedButton.styleFrom(minimumSize: const Size.fromHeight(48)),
                    ),
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: OutlinedButton(
                      onPressed: () {},
                      style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(48)),
                      child: const Text('Messaggio'),
                    ),
                  ),
                ],
              ),
            ],
          ),
        );
      }
    }
    
    class _StatItem extends StatelessWidget {
      const _StatItem({required this.value, required this.label});
    
      final String value;
      final String label;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            const SizedBox(height: 2),
            Text(label, style: const TextStyle(color: Colors.black54, fontSize: 13)),
          ],
        );
      }
    }
    
    // Uso nella HomePage
    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: const Color(0xFFF2F3F7),
          appBar: AppBar(title: const Text('Card profilo')),
          body: SafeArea(
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(16),
              child: Center(
                child: ConstrainedBox(
                  constraints: const BoxConstraints(maxWidth: 480),
                  child: const ProfileCard(
                    name: 'Giulia Ferrari',
                    role: 'Mobile Developer specializzata in Flutter',
                    posts: '128',
                    followers: '4.2k',
                    following: '310',
                  ),
                ),
              ),
            ),
          ),
        );
      }
    }

    Risultato atteso

    La card profilo è completa, riutilizzabile con parametri diversi, non va in overflow su schermi piccoli e resta ben proporzionata su tablet e desktop.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!