Perché usare gli skeleton loader
Quando un'app carica dati dalla rete, mostrare uno spinner al centro dello schermo è la soluzione più semplice ma spesso poco soddisfacente. Gli skeleton loader (o "scheletri") riproducono la struttura della UI finale con forme grigie animate, comunicando all'utente cosa sta per apparire e riducendo la percezione del tempo di attesa.
L'effetto shimmer — quel bagliore che scorre sui placeholder — è ormai uno standard adottato da app come Facebook, LinkedIn e YouTube. In Flutter possiamo implementarlo manualmente, ma il pacchetto skeletonizer semplifica enormemente il lavoro: trasforma automaticamente qualsiasi widget in uno scheletro, senza duplicare i layout.
Installazione
Aggiungi la dipendenza al pubspec.yaml:
dependencies:
skeletonizer: ^1.4.2
Poi esegui:
flutter pub get
Il concetto chiave: un solo layout
A differenza degli approcci tradizionali, dove si scrive un widget separato per lo stato di caricamento, Skeletonizer usa lo stesso layout sia per i dati reali sia per lo scheletro. Basta avvolgere l'albero dei widget in un Skeletonizer e impostare la proprietà enabled.
import 'package:flutter/material.dart';
import 'package:skeletonizer/skeletonizer.dart';
class ArticoloList extends StatelessWidget {
final bool isLoading;
final List<Articolo> articoli;
const ArticoloList({
super.key,
required this.isLoading,
required this.articoli,
});
@override
Widget build(BuildContext context) {
return Skeletonizer(
enabled: isLoading,
child: ListView.builder(
itemCount: isLoading ? 6 : articoli.length,
itemBuilder: (context, index) {
final articolo = isLoading
? const Articolo.placeholder()
: articoli[index];
return Card(
child: ListTile(
leading: const CircleAvatar(),
title: Text(articolo.titolo),
subtitle: Text(articolo.autore),
),
);
},
),
);
}
}
Quando enabled è true, Skeletonizer disegna automaticamente forme grigie animate al posto di testi, avatar e immagini. Quando diventa false, viene mostrato il contenuto reale con una transizione fluida.
Dati placeholder
Durante il caricamento serve comunque una struttura da renderizzare. Un pattern comune è definire un factory che genera dati fittizi:
class Articolo {
final String titolo;
final String autore;
const Articolo({required this.titolo, required this.autore});
factory Articolo.placeholder() => const Articolo(
titolo: 'Titolo di esempio articolo',
autore: 'Nome autore',
);
}
Il testo non conta: Skeletonizer misura solo la larghezza per disegnare la barra grigia.
Personalizzare l'effetto shimmer
Il comportamento predefinito usa un'animazione shimmer, ma possiamo configurarla tramite la proprietà effect:
Skeletonizer(
enabled: isLoading,
effect: ShimmerEffect(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
duration: const Duration(seconds: 1),
),
child: myWidget,
)
Altri effetti disponibili sono:
PulseEffect: un dissolvimento pulsante, meno appariscente dello shimmer;SoldColorEffect: colore statico senza animazione, utile per ridurre il consumo di risorse.
Controllo fine con le annotazioni
A volte alcuni widget non vanno "scheletrizzati" o richiedono forme specifiche. Skeletonizer offre delle annotazioni:
Column(
children: [
// Ignora questo widget: resta sempre visibile
Skeleton.keep(
child: const Icon(Icons.favorite),
),
// Nasconde completamente durante il loading
Skeleton.ignore(
child: const Divider(),
),
// Forza uno scheletro rettangolare
Skeleton.leaf(
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
],
)
Skeleton.keep: mantiene il widget renderizzato normalmente;Skeleton.ignore: lo esclude dallo scheletro;Skeleton.leaf: lo tratta come un nodo foglia unico, evitando che i figli generino scheletri separati;Skeleton.replace: sostituisce il widget con una forma di dimensioni fisse durante il loading.
Integrazione con dati asincroni
Nella pratica lo stato di caricamento arriva da una chiamata di rete. Ecco un esempio con FutureBuilder:
FutureBuilder<List<Articolo>>(
future: repository.caricaArticoli(),
builder: (context, snapshot) {
final isLoading = !snapshot.hasData;
final articoli = snapshot.data ?? [];
return ArticoloList(
isLoading: isLoading,
articoli: articoli,
);
},
)
Lo stesso approccio si adatta perfettamente a Riverpod, BLoC o qualunque gestore di stato: è sufficiente mappare lo stato "loading" sulla proprietà enabled.
Best practice
- Mantieni gli scheletri fedeli al layout finale: un numero di righe o dimensioni molto diverse crea un fastidioso "salto" quando i dati arrivano.
- Non esagerare con la durata: uno skeleton mostrato per meno di 300 ms genera un flash sgradevole. Valuta un tempo minimo di visualizzazione.
- Usa
PulseEffectper liste lunghe: lo shimmer su molti elementi può risultare pesante e visivamente rumoroso. - Testa in dark mode: adatta
baseColorehighlightColoral tema corrente per evitare contrasti eccessivi.
Conclusioni
Skeletonizer elimina la duplicazione dei layout tipica degli skeleton loader tradizionali, rendendo l'implementazione quasi immediata. Con poche righe di codice si ottiene un'esperienza di caricamento moderna e professionale, migliorando la percezione di velocità dell'app senza appesantire la codebase.
