Decorare i widget in Flutter con BoxDecoration: bordi, ombre e gradienti

Foto di Codioful (Formerly Gradienta) su Unsplash

GuidePrincipiante35 min Flutter 3.x

Decorare i widget in Flutter con BoxDecoration: bordi, ombre e gradienti

Quando si inizia con Flutter, il Container è il widget che si usa più spesso: un rettangolo in cui mettere del contenuto. Ma finché gli si assegna solo un color, il risultato resta piatto e anonimo.

La vera potenza arriva con BoxDecoration: una singola classe che permette di definire colore di sfondo, bordi, angoli arrotondati, ombre, gradienti e immagini, senza dover installare alcun pacchetto esterno.

In questo tutorial costruiremo, passo dopo passo, una card di profilo partendo da un rettangolo grigio e arrivando a un componente rifinito e riutilizzabile. Impareremo anche gli errori più comuni (come il classico crash "Cannot provide both a color and a decoration") e come evitarli.

Cosa ti serve: Flutter 3.x installato, un editor (VS Code o Android Studio) e un progetto creato con flutter create decorazioni_demo.

  1. 1

    Preparare il progetto e il primo Container decorato

    Crea un nuovo progetto e sostituisci il contenuto di lib/main.dart con il codice qui sotto. Partiamo da una schermata con un Container centrale colorato.

    Attenzione all'errore più comune: un Container non può avere contemporaneamente la proprietà color e la proprietà decoration. Se le usi entrambe, l'app va in crash con il messaggio:

    Cannot provide both a color and a decoration. To provide both, use "decoration: BoxDecoration(color: color)".

    La regola è semplice: appena ti serve una decorazione, sposta il colore dentro BoxDecoration.

    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: 'Decorazioni Demo',
          debugShowCheckedModeBanner: false,
          theme: ThemeData(useMaterial3: true),
          home: const HomePage(),
        );
      }
    }
    
    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: const Color(0xFFF2F4F8),
          appBar: AppBar(title: const Text('BoxDecoration')),
          body: Center(
            child: Container(
              width: 260,
              height: 140,
              // NON usare anche "color:" qui fuori: il colore va dentro la decoration
              decoration: const BoxDecoration(
                color: Colors.white,
              ),
              child: const Center(child: Text('Il mio box')),
            ),
          ),
        );
      }
    }

    Risultato atteso

    Avviando l'app vedi un rettangolo bianco 260x140 al centro di uno sfondo grigio chiaro, con la scritta "Il mio box".

  2. 2

    Aggiungere bordi e angoli arrotondati

    Due proprietà di BoxDecoration fanno subito la differenza:

    • border: il contorno del box. Border.all(...) applica lo stesso bordo su tutti i lati, ma puoi anche usare Border(top: BorderSide(...)) per un solo lato.
    • borderRadius: arrotonda gli angoli. BorderRadius.circular(16) li arrotonda tutti allo stesso modo, BorderRadius.only(...) ti permette di scegliere quali.

    Nota: borderRadius funziona solo se la forma è un rettangolo (shape: BoxShape.rectangle, che è il default). Se imposti shape: BoxShape.circle devi rimuovere borderRadius, altrimenti ottieni un errore in fase di build.

    Sostituisci il Container del passo precedente con questo.

    Container(
      width: 260,
      height: 140,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(20),
        border: Border.all(
          color: Colors.indigo.shade200,
          width: 2,
        ),
      ),
      child: const Center(
        child: Text(
          'Box con bordo',
          style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
        ),
      ),
    )
    
    // Variante: solo gli angoli superiori arrotondati
    // borderRadius: const BorderRadius.only(
    //   topLeft: Radius.circular(24),
    //   topRight: Radius.circular(24),
    // ),
    
    // Variante: cerchio perfetto (niente borderRadius!)
    // decoration: BoxDecoration(
    //   shape: BoxShape.circle,
    //   color: Colors.indigo,
    // ),

    Risultato atteso

    Il box ora ha angoli arrotondati di 20 pixel e un sottile bordo indaco di 2 pixel.

  3. 3

    Dare profondità con le ombre (BoxShadow)

    La proprietà boxShadow accetta una lista di BoxShadow: puoi quindi sovrapporne più di una per ottenere ombre morbide e realistiche.

    Le proprietà da conoscere:

    Proprietà A cosa serve
    color Colore dell'ombra, quasi sempre con opacità bassa
    offset Spostamento in X e Y (Offset(0, 8) = ombra verso il basso)
    blurRadius Quanto l'ombra è sfocata
    spreadRadius Quanto l'ombra si espande prima di sfocare

    Un buon punto di partenza per una card è un'ombra scura al 8-12% di opacità, leggermente spostata verso il basso.

    Container(
      width: 260,
      height: 140,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(20),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.10),
            offset: const Offset(0, 8),
            blurRadius: 20,
            spreadRadius: 0,
          ),
          BoxShadow(
            color: Colors.black.withOpacity(0.04),
            offset: const Offset(0, 2),
            blurRadius: 4,
          ),
        ],
      ),
      child: const Center(child: Text('Box con ombra')),
    )

    Risultato atteso

    Il box appare sollevato dallo sfondo, con un'ombra morbida sotto: l'effetto è simile a quello di una Card di Material Design, ma completamente sotto il tuo controllo.

  4. 4

    Sfumature con LinearGradient e RadialGradient

    Il campo gradient sostituisce il color: se li imposti entrambi, il gradiente vince e il colore viene ignorato (quindi meglio rimuoverlo per chiarezza).

    I gradienti più usati sono:

    • LinearGradient: sfumatura lungo una direzione, definita da begin e end (usa le costanti Alignment.topLeft, Alignment.bottomRight, ecc.).
    • RadialGradient: sfumatura circolare che parte dal centro.
    • SweepGradient: sfumatura "a ventaglio" attorno a un punto.

    Con stops puoi decidere in quale percentuale del percorso (da 0.0 a 1.0) si colloca ogni colore.

    Container(
      width: 260,
      height: 140,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(20),
        gradient: const LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            Color(0xFF6A11CB),
            Color(0xFF2575FC),
          ],
          stops: [0.0, 1.0],
        ),
        boxShadow: [
          BoxShadow(
            color: const Color(0xFF2575FC).withOpacity(0.35),
            offset: const Offset(0, 10),
            blurRadius: 24,
          ),
        ],
      ),
      child: const Center(
        child: Text(
          'Gradiente',
          style: TextStyle(
            color: Colors.white,
            fontSize: 20,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    )

    Risultato atteso

    Il box mostra una sfumatura dal viola al blu in diagonale, con un'ombra colorata coordinata che accentua l'effetto.

  5. 5

    Immagine di sfondo con DecorationImage

    BoxDecoration può usare un'immagine come sfondo tramite image: DecorationImage(...). È utile per header e copertine.

    Punti chiave:

    • fit: BoxFit.cover riempie lo spazio mantenendo le proporzioni (è quasi sempre la scelta giusta).
    • colorFilter permette di scurire l'immagine così che il testo sopra resti leggibile.
    • L'immagine viene automaticamente ritagliata secondo il borderRadius della decorazione: non serve un ClipRRect esterno.

    Per usare un'immagine locale ricordati di dichiararla in pubspec.yaml:

    flutter:
      assets:
        - assets/images/
    

    In alternativa puoi usare NetworkImage('https://...') per un'immagine remota.

    Container(
      width: 260,
      height: 160,
      padding: const EdgeInsets.all(16),
      alignment: Alignment.bottomLeft,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(20),
        image: DecorationImage(
          image: const NetworkImage(
            'https://picsum.photos/600/400',
          ),
          fit: BoxFit.cover,
          colorFilter: ColorFilter.mode(
            Colors.black.withOpacity(0.35),
            BlendMode.darken,
          ),
        ),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.20),
            offset: const Offset(0, 8),
            blurRadius: 18,
          ),
        ],
      ),
      child: const Text(
        'Copertina',
        style: TextStyle(
          color: Colors.white,
          fontSize: 22,
          fontWeight: FontWeight.bold,
        ),
      ),
    )

    Risultato atteso

    Il box mostra una foto di sfondo ritagliata con angoli arrotondati, leggermente scurita, con la scritta bianca "Copertina" in basso a sinistra.

  6. 6

    Mettere tutto insieme: una card di profilo riutilizzabile

    Ora combiniamo quanto imparato in un widget riutilizzabile. Tre buone pratiche da seguire fin da subito:

    1. Estrai il widget in una classe separata (ProfileCard) invece di annidare tutto nel build.
    2. Usa const dove possibile: le decorazioni costanti vengono create una sola volta e migliorano le performance.
    3. Definisci le decorazioni ricorrenti in un unico punto (una classe AppDecorations con costanti statiche), così mantieni lo stile coerente in tutta l'app.

    Sostituisci l'intero HomePage con il codice seguente.

    class AppDecorations {
      const AppDecorations._();
    
      static BoxDecoration card = BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(20),
        border: Border.all(color: const Color(0xFFE6E8EF)),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.08),
            offset: const Offset(0, 6),
            blurRadius: 16,
          ),
        ],
      );
    
      static const BoxDecoration avatar = BoxDecoration(
        shape: BoxShape.circle,
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [Color(0xFF6A11CB), Color(0xFF2575FC)],
        ),
      );
    }
    
    class ProfileCard extends StatelessWidget {
      const ProfileCard({
        super.key,
        required this.nome,
        required this.ruolo,
        required this.iniziali,
      });
    
      final String nome;
      final String ruolo;
      final String iniziali;
    
      @override
      Widget build(BuildContext context) {
        return Container(
          padding: const EdgeInsets.all(16),
          decoration: AppDecorations.card,
          child: Row(
            children: [
              Container(
                width: 56,
                height: 56,
                alignment: Alignment.center,
                decoration: AppDecorations.avatar,
                child: Text(
                  iniziali,
                  style: const TextStyle(
                    color: Colors.white,
                    fontWeight: FontWeight.bold,
                    fontSize: 18,
                  ),
                ),
              ),
              const SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      nome,
                      style: const TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      ruolo,
                      style: TextStyle(color: Colors.grey.shade600),
                    ),
                  ],
                ),
              ),
              const Icon(Icons.chevron_right),
            ],
          ),
        );
      }
    }
    
    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: const Color(0xFFF2F4F8),
          appBar: AppBar(title: const Text('BoxDecoration')),
          body: ListView(
            padding: const EdgeInsets.all(16),
            children: const [
              ProfileCard(
                nome: 'Giulia Rossi',
                ruolo: 'Flutter Developer',
                iniziali: 'GR',
              ),
              SizedBox(height: 12),
              ProfileCard(
                nome: 'Marco Bianchi',
                ruolo: 'UI Designer',
                iniziali: 'MB',
              ),
            ],
          ),
        );
      }
    }

    Risultato atteso

    Vedi una lista di card bianche con bordo sottile e ombra morbida, ognuna con un avatar circolare a gradiente contenente le iniziali, nome, ruolo e una freccia a destra.

  7. 7

    Rendere la card cliccabile senza perdere la decorazione

    Un Container decorato non reagisce al tocco. Se avvolgi tutto in un InkWell, però, l'effetto ripple viene disegnato sotto il Container e non si vede.

    La soluzione corretta in Material Design è:

    1. Usare un Material con color: Colors.transparent (o un Ink con la decorazione).
    2. Mettere l'InkWell sopra, con lo stesso borderRadius della decorazione, così il ripple resta dentro gli angoli arrotondati.

    Il trucco più pulito è il widget Ink, che accetta una decoration esattamente come Container e permette all'InkWell di disegnarci sopra.

    // Dentro ProfileCard, sostituisci il Container esterno con:
    
    Material(
      color: Colors.transparent,
      child: Ink(
        decoration: AppDecorations.card,
        child: InkWell(
          borderRadius: BorderRadius.circular(20), // stesso raggio della decoration
          onTap: () {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Hai toccato $nome')),
            );
          },
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              // ...stesso contenuto del passo precedente
            ),
          ),
        ),
      ),
    )

    Risultato atteso

    Toccando una card compare l'effetto ripple contenuto negli angoli arrotondati e viene mostrata una SnackBar con il nome selezionato. Hai ora una card decorata, riutilizzabile e interattiva, costruita solo con widget del framework.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!