Sovrapporre widget in Flutter con Stack, Positioned e Align

Foto di Scott Greer su Unsplash

GuidePrincipiante30 min Flutter 3.x

Sovrapporre widget in Flutter con Stack, Positioned e Align

Row e Column ti permettono di disporre i widget uno accanto all'altro o uno sotto l'altro, ma prima o poi ti servirà sovrapporre elementi: un testo sopra una foto, un badge con il numero di notifiche sopra un'icona, un pulsante flottante ancorato in un angolo di una card.

In Flutter tutto questo si fa con il widget Stack, che impila i figli uno sopra l'altro come fogli di carta. Per posizionarli con precisione useremo Positioned e Align.

In questo tutorial costruiremo passo dopo passo una schermata con:

  • una card immagine con titolo sovrapposto e sfumatura
  • un avatar che "sborda" dal contenitore
  • un badge di notifica su un'icona

Al termine saprai leggere e scrivere qualsiasi layout a livelli senza impazzire con gli overflow.

  1. 1

    Creare il progetto e capire come funziona Stack

    Crea un nuovo progetto (oppure svuota il main.dart di uno esistente):

    flutter create stack_demo
    cd stack_demo
    

    Stack accetta una lista di children: il primo figlio è quello più in basso, l'ultimo è quello disegnato sopra tutti gli altri. Per default i figli vengono allineati in alto a sinistra (alignment: AlignmentDirectional.topStart).

    Un concetto chiave: nello Stack i figli si dividono in due categorie.

    • Non posizionati (widget normali): determinano la dimensione dello Stack e vengono allineati secondo la proprietà alignment.
    • Posizionati (avvolti in Positioned): vengono ancorati ai bordi dello Stack tramite top, right, bottom, left.

    Partiamo da un esempio minimo con tre quadrati colorati sovrapposti.

    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: 'Stack Demo',
          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('Stack, Positioned e Align')),
          body: Center(
            child: Stack(
              children: [
                Container(width: 200, height: 200, color: Colors.indigo),
                Container(width: 140, height: 140, color: Colors.orange),
                Container(width: 80, height: 80, color: Colors.white),
              ],
            ),
          ),
        );
      }
    }

    Risultato atteso

    Vedi tre quadrati sovrapposti, tutti allineati in alto a sinistra: il quadrato bianco (ultimo figlio) è disegnato sopra a tutti.

  2. 2

    Controllare l'allineamento con la proprietà alignment

    Se non usi Positioned, tutti i figli seguono la proprietà alignment dello Stack. Prova a impostarla su Alignment.center: i tre quadrati diventeranno concentrici.

    I valori più usati sono Alignment.center, Alignment.topRight, Alignment.bottomLeft, ecc. Puoi anche usare coordinate personalizzate con Alignment(x, y) dove x e y vanno da -1.0 (sinistra/alto) a 1.0 (destra/basso).

    Un'altra proprietà utile è fit:

    • StackFit.loose (default): i figli usano la dimensione che preferiscono;
    • StackFit.expand: i figli non posizionati vengono espansi per riempire lo Stack.

    Infine clipBehavior decide se tagliare (Clip.hardEdge, default) o meno i figli che escono dai bordi: ci tornerà utile più avanti per l'avatar sporgente.

    Center(
      child: Stack(
        alignment: Alignment.center,
        children: [
          Container(width: 200, height: 200, color: Colors.indigo),
          Container(width: 140, height: 140, color: Colors.orange),
          const Text(
            'Ciao!',
            style: TextStyle(
              color: Colors.white,
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
          ),
        ],
      ),
    )

    Risultato atteso

    I quadrati sono ora concentrici e la scritta "Ciao!" appare esattamente al centro, sopra il quadrato arancione.

  3. 3

    Ancorare i figli con Positioned

    Positioned funziona solo come figlio diretto di uno Stack e permette di ancorare un widget ai bordi indicando top, right, bottom, left.

    Regole pratiche:

    • se indichi left e right, la larghezza viene calcolata di conseguenza (non serve width);
    • se indichi solo left, devi dare una larghezza al figlio (o lasciare che se la calcoli da solo);
    • Positioned.fill() è una scorciatoia per top: 0, left: 0, right: 0, bottom: 0.

    Attenzione: uno Stack che contiene solo figli Positioned non sa quanto essere grande e collassa. In quel caso avvolgilo in un SizedBox, un Container con dimensioni o un AspectRatio.

    Ecco una card con un'immagine, una sfumatura che rende leggibile il testo e un titolo ancorato in basso.

    class ImageCard extends StatelessWidget {
      const ImageCard({super.key});
    
      @override
      Widget build(BuildContext context) {
        return ClipRRect(
          borderRadius: BorderRadius.circular(16),
          child: SizedBox(
            height: 200,
            width: double.infinity,
            child: Stack(
              fit: StackFit.expand,
              children: [
                // 1. Livello di fondo: l'immagine
                Image.network(
                  'https://picsum.photos/800/400',
                  fit: BoxFit.cover,
                ),
                // 2. Sfumatura per rendere leggibile il testo
                const DecoratedBox(
                  decoration: BoxDecoration(
                    gradient: LinearGradient(
                      begin: Alignment.topCenter,
                      end: Alignment.bottomCenter,
                      colors: [Colors.transparent, Colors.black87],
                      stops: [0.5, 1.0],
                    ),
                  ),
                ),
                // 3. Titolo ancorato in basso a sinistra
                const Positioned(
                  left: 16,
                  right: 16,
                  bottom: 16,
                  child: Text(
                    'Tramonto sulle Dolomiti',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 20,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }

    Risultato atteso

    Una card con angoli arrotondati: l'immagine riempie tutto lo spazio, la parte bassa sfuma verso il nero e il titolo bianco è perfettamente leggibile.

  4. 4

    Creare un badge di notifica con Positioned

    Il classico pallino rosso con il numero di notifiche sopra un'icona è l'uso più comune di Stack.

    Il trucco è usare un valore negativo o molto piccolo per top e right, così il badge "morde" l'angolo dell'icona. Se usi valori negativi ricordati di impostare clipBehavior: Clip.none, altrimenti la parte fuori dai bordi verrà tagliata.

    Nota anche il pattern if (count > 0) dentro la lista di children: è la collection if di Dart, comodissima per mostrare un widget solo a certe condizioni.

    class NotificationIcon extends StatelessWidget {
      const NotificationIcon({super.key, required this.count});
    
      final int count;
    
      @override
      Widget build(BuildContext context) {
        return Stack(
          clipBehavior: Clip.none,
          children: [
            IconButton(
              icon: const Icon(Icons.notifications_outlined, size: 32),
              onPressed: () {},
            ),
            if (count > 0)
              Positioned(
                top: 4,
                right: 2,
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                  constraints: const BoxConstraints(minWidth: 20),
                  decoration: BoxDecoration(
                    color: Colors.red,
                    borderRadius: BorderRadius.circular(10),
                    border: Border.all(color: Colors.white, width: 2),
                  ),
                  child: Text(
                    count > 99 ? '99+' : '$count',
                    textAlign: TextAlign.center,
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 11,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
          ],
        );
      }
    }

    Risultato atteso

    L'icona della campanella mostra un badge rosso in alto a destra con il numero di notifiche; se `count` è 0 il badge sparisce.

  5. 5

    Usare Align e l'avatar che sborda dal contenitore

    Align è l'alternativa "relativa" a Positioned: invece di misure in pixel usa coordinate da -1.0 a 1.0, quindi si adatta automaticamente a qualsiasi dimensione. È perfetto quando vuoi qualcosa "in basso a destra" indipendentemente dalle dimensioni dello schermo.

    Un caso classico è l'header di un profilo: una copertina colorata e un avatar circolare che sborda a metà sul contenuto sottostante. Per farlo si usa un Positioned con bottom negativo e clipBehavior: Clip.none, ricordando di lasciare spazio sotto (qui con un Padding/SizedBox).

    Attenzione: le aree che escono dallo Stack con Clip.none sono visibili ma non ricevono i tocchi fuori dai limiti del genitore. Se il widget deve essere cliccabile, meglio allargare lo Stack invece di sfondarne i bordi.

    class ProfileHeader extends StatelessWidget {
      const ProfileHeader({super.key});
    
      @override
      Widget build(BuildContext context) {
        return SizedBox(
          height: 190, // 140 di copertina + 50 di sporgenza
          child: Stack(
            clipBehavior: Clip.none,
            children: [
              Container(
                height: 140,
                width: double.infinity,
                decoration: const BoxDecoration(
                  gradient: LinearGradient(
                    colors: [Color(0xFF3F51B5), Color(0xFF7986CB)],
                  ),
                ),
              ),
              // Pulsante "modifica" allineato con Align
              const Align(
                alignment: Alignment(0.9, -0.6),
                child: Icon(Icons.edit, color: Colors.white),
              ),
              // Avatar che sborda dalla copertina
              Positioned(
                bottom: 0,
                left: 24,
                child: Container(
                  padding: const EdgeInsets.all(4),
                  decoration: const BoxDecoration(
                    color: Colors.white,
                    shape: BoxShape.circle,
                  ),
                  child: const CircleAvatar(
                    radius: 44,
                    backgroundImage: NetworkImage('https://i.pravatar.cc/200'),
                  ),
                ),
              ),
            ],
          ),
        );
      }
    }

    Risultato atteso

    Un header con sfondo sfumato, un'icona di modifica in alto a destra e un avatar circolare con bordo bianco che si sovrappone a metà tra copertina e contenuto.

  6. 6

    Mettere tutto insieme in una schermata

    Ora componiamo i pezzi in un'unica pagina. Nota che Stack si combina benissimo con Column, ListView e gli altri widget di layout: basta ricordare che ogni Stack crea un piccolo "mondo" a livelli con le sue dimensioni.

    Alcune buone pratiche finali:

    • estrai ogni blocco in un widget separato (come abbiamo fatto): il codice resta leggibile e Flutter può ottimizzare i rebuild;
    • usa const ovunque possibile;
    • se ti serve un solo figlio allineato dentro un Container, non serve Stack: basta la proprietà alignment del Container;
    • per posizionare qualcosa rispetto al padre fuori da uno Stack, usa Align o Positioned solo dentro Stack, altrimenti otterrai l'errore "Incorrect use of ParentDataWidget".
    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Stack in pratica'),
            actions: const [
              NotificationIcon(count: 5),
              SizedBox(width: 8),
            ],
          ),
          body: ListView(
            children: const [
              ProfileHeader(),
              Padding(
                padding: EdgeInsets.all(16),
                child: Text(
                  'Marco Rossi',
                  style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
                ),
              ),
              Padding(
                padding: EdgeInsets.symmetric(horizontal: 16),
                child: ImageCard(),
              ),
              SizedBox(height: 24),
            ],
          ),
        );
      }
    }

    Risultato atteso

    Una schermata completa con badge di notifica nell'AppBar, header di profilo con avatar sporgente e card immagine con titolo sovrapposto, il tutto scorrevole.

  7. 7

    Risolvere gli errori più comuni con Stack

    Ecco i tre problemi che incontrerai quasi certamente e come risolverli.

    1. "Incorrect use of ParentDataWidget"

    Hai usato Positioned fuori da uno Stack (magari dentro una Column, o dentro un Container intermedio). Positioned deve essere figlio diretto di Stack.

    2. Lo Stack ha altezza zero o si espande all'infinito

    Succede quando tutti i figli sono Positioned, oppure quando lo Stack è dentro una Column senza vincoli. Soluzione: dai una dimensione esplicita con SizedBox, AspectRatio o Expanded, oppure lascia almeno un figlio non posizionato che definisca le dimensioni.

    3. Un figlio viene tagliato ai bordi

    È il clipBehavior di default (Clip.hardEdge). Imposta clipBehavior: Clip.none se vuoi che il widget sporga.

    Per ispezionare tutto questo apri i Flutter DevTools e usa il Widget Inspector: vedrai chiaramente i confini dello Stack e di ogni livello.

    // ❌ SBAGLIATO: Positioned dentro una Column
    Column(
      children: [
        Positioned(top: 10, child: Text('Errore')),
      ],
    )
    
    // ✅ CORRETTO: Positioned figlio diretto di Stack
    SizedBox(
      height: 120,
      child: Stack(
        children: const [
          Positioned(top: 10, left: 10, child: Text('Ok!')),
        ],
      ),
    )
    
    // ✅ Stack dentro Column: dai un'altezza o usa Expanded
    Column(
      children: [
        Expanded(
          child: Stack(
            children: const [
              Positioned.fill(child: ColoredBox(color: Colors.amber)),
              Positioned(bottom: 16, right: 16, child: Icon(Icons.star)),
            ],
          ),
        ),
      ],
    )

    Risultato atteso

    L'app compila senza errori di layout e sai riconoscere a colpo d'occhio la causa dei problemi più frequenti con Stack.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!