Cosa sono i Sliver

In Flutter, un Sliver è una porzione di un'area scorrevole. Il termine inglese significa letteralmente "scheggia" o "fettina": ogni Sliver rappresenta infatti una sezione che può comportarsi in modo diverso durante lo scorrimento. Mentre widget come ListView o GridView coprono i casi più comuni, quando hai bisogno di combinare più elementi scorrevoli con effetti sofisticati (app bar che si comprime, header che restano agganciati, liste e griglie nella stessa vista) i Sliver diventano lo strumento giusto.

Il cuore di tutto è il widget CustomScrollView, che accetta una lista di Sliver e li dispone lungo un unico asse di scorrimento condiviso.

Il primo CustomScrollView

Vediamo un esempio base che combina un'app bar collassabile e una lista:

CustomScrollView(
  slivers: [
    SliverAppBar(
      expandedHeight: 200,
      pinned: true,
      flexibleSpace: FlexibleSpaceBar(
        title: const Text('Esplora'),
        background: Image.network(
          'https://picsum.photos/600/300',
          fit: BoxFit.cover,
        ),
      ),
    ),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(
          leading: const Icon(Icons.star),
          title: Text('Elemento $index'),
        ),
        childCount: 30,
      ),
    ),
  ],
)

Nota le proprietà di SliverAppBar:

  • expandedHeight: l'altezza massima quando è completamente espansa.
  • pinned: se true, l'app bar resta visibile (agganciata) in alto anche dopo il collasso.
  • floating: se true, l'app bar riappare appena scorri verso l'alto, senza dover arrivare in cima.
  • snap (usabile con floating: true): completa automaticamente l'animazione di apertura/chiusura.

SliverList e SliverGrid

I due Sliver più usati per i contenuti sono SliverList e SliverGrid. Entrambi accettano un delegate che descrive come costruire i figli.

Per una griglia:

SliverGrid(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 3,
    mainAxisSpacing: 8,
    crossAxisSpacing: 8,
    childAspectRatio: 1,
  ),
  delegate: SliverChildBuilderDelegate(
    (context, index) => Container(
      color: Colors.teal[(index % 9 + 1) * 100],
      alignment: Alignment.center,
      child: Text('$index'),
    ),
    childCount: 24,
  ),
)

Esiste anche il pratico costruttore SliverList.builder e SliverGrid.builder introdotti per semplificare la sintassi, evitando di scrivere esplicitamente il delegate:

SliverList.builder(
  itemCount: 30,
  itemBuilder: (context, index) => ListTile(
    title: Text('Voce $index'),
  ),
)

Inserire widget non scorrevoli con SliverToBoxAdapter

Spesso vuoi inserire un widget normale (non un Sliver) all'interno di un CustomScrollView, ad esempio un banner o un titolo di sezione. Per farlo si usa SliverToBoxAdapter:

SliverToBoxAdapter(
  child: Padding(
    padding: const EdgeInsets.all(16),
    child: Text(
      'In evidenza',
      style: Theme.of(context).textTheme.headlineSmall,
    ),
  ),
)

Fai attenzione: SliverToBoxAdapter costruisce il suo figlio tutto in una volta, quindi non è adatto a liste lunghe. Per quelle usa sempre SliverList.

Header agganciati con SliverPersistentHeader

Uno degli effetti più richiesti è l'header di sezione che resta "appiccicato" in cima mentre scorri (il classico sticky header). Si ottiene con SliverPersistentHeader e un delegate personalizzato:

class _SezioneHeader extends SliverPersistentHeaderDelegate {
  final String titolo;
  _SezioneHeader(this.titolo);

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    return Container(
      color: Theme.of(context).colorScheme.surface,
      alignment: Alignment.centerLeft,
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Text(
        titolo,
        style: Theme.of(context).textTheme.titleMedium,
      ),
    );
  }

  @override
  double get maxExtent => 48;

  @override
  double get minExtent => 48;

  @override
  bool shouldRebuild(_SezioneHeader oldDelegate) =>
      oldDelegate.titolo != titolo;
}

E poi nel CustomScrollView:

SliverPersistentHeader(
  pinned: true,
  delegate: _SezioneHeader('Categoria A'),
)

Con pinned: true l'header rimane agganciato in cima; con floating: true ricompare allo scroll verso l'alto.

Aggiungere padding ai Sliver

Non puoi avvolgere un Sliver in un normale Padding: ti serve SliverPadding:

SliverPadding(
  padding: const EdgeInsets.all(16),
  sliver: SliverList.builder(
    itemCount: 10,
    itemBuilder: (context, i) => Text('Riga $i'),
  ),
)

Riempire lo spazio rimanente

Quando una lista è più corta dello schermo e vuoi che un widget occupi lo spazio residuo (utile per stati vuoti), usa SliverFillRemaining:

SliverFillRemaining(
  hasScrollBody: false,
  child: Center(
    child: Text('Non ci sono altri elementi'),
  ),
)

Best practice

  • Usa i builder: con SliverChildBuilderDelegate o i costruttori .builder i figli vengono creati pigramente (lazy), risparmiando memoria.
  • Evita SliverToBoxAdapter per le liste: rende inutile la costruzione lazy.
  • Un solo asse di scorrimento: tutti i Sliver in un CustomScrollView condividono lo stesso ScrollController. Per scroll annidati su assi diversi valuta NestedScrollView.
  • Definisci bene min/maxExtent nei delegate persistenti: valori errati causano errori di layout.

Conclusioni

I Sliver sono la chiave per costruire interfacce di scorrimento veramente flessibili in Flutter. Con CustomScrollView puoi combinare app bar collassabili, header agganciati, liste, griglie e widget statici in un'unica esperienza fluida. Una volta compreso il meccanismo di delegate e extent, avrai a disposizione un sistema potente per dare carattere alle tue schermate scorrevoli.