Il problema: ricompilare tutta l'app per vedere un bottone
Chi sviluppa in Flutter conosce bene il ciclo: modifichi un widget in fondo a un flusso di navigazione, lanci l'app, fai login, navighi tra tre schermate e finalmente vedi la modifica. L'hot reload aiuta moltissimo, ma non elimina il costo di dover raggiungere lo stato in cui quel widget è visibile.
Chi arriva da Android con Jetpack Compose, o da SwiftUI, conosce la soluzione: le preview. Dalla versione 3.35 Flutter ha introdotto il Widget Previewer, uno strumento che consente di annotare funzioni che restituiscono widget e vederle renderizzate in una griglia aggiornata in tempo reale, senza avviare l'applicazione completa.
Nota sulle versioni: la funzionalità è stata rilasciata in preview e la sua API è ancora in evoluzione. Verifica con
flutter --versiondi essere almeno sulla 3.35 (meglio se su una release più recente) e consultaflutter widget-preview --helpper i flag supportati dalla tua versione.
Avviare il previewer
Il punto di ingresso è un comando della CLI, da eseguire nella root del progetto:
flutter widget-preview start
Al primo avvio Flutter genera un preview scaffold (una piccola applicazione separata, in una cartella dedicata nel progetto) che si occupa di scansionare il codice alla ricerca delle preview, compilarle e mostrarle. L'ambiente viene aperto come app desktop o nel browser a seconda della configurazione della macchina; gli IDE con il plugin Flutter aggiornato espongono un pulsante dedicato per aprirlo in un pannello laterale.
Per fermarlo e ripulire gli artefatti generati:
flutter widget-preview clean
È buona norma aggiungere la cartella generata al .gitignore, per non versionare file di build.
La prima preview
Una preview è semplicemente una funzione top-level (o un metodo statico) senza parametri obbligatori che restituisce un Widget, annotata con @Preview().
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
import 'package:my_app/ui/widgets/product_card.dart';
import 'package:my_app/domain/product.dart';
@Preview(name: 'Product card - disponibile')
Widget productCardAvailable() {
return ProductCard(
product: const Product(
id: '1',
title: 'Scarpe da trail',
price: 89.90,
available: true,
),
onTap: () {},
);
}
@Preview(name: 'Product card - esaurito')
Widget productCardSoldOut() {
return ProductCard(
product: const Product(
id: '2',
title: 'Giacca antivento',
price: 129.00,
available: false,
),
onTap: () {},
);
}
Salvando il file, il previewer ricompila e mostra le due card affiancate. Ogni modifica al widget ProductCard si riflette immediatamente in entrambe le anteprime: è l'equivalente visivo di uno unit test, ma per la UI.
Il punto chiave è che la funzione deve essere risolvibile staticamente: niente parametri richiesti, niente chiusure costruite a runtime. Puoi però usare costanti, factory e dati finti definiti nel file stesso.
Personalizzare l'anteprima
L'annotazione Preview accetta diversi parametri per controllare il contesto di rendering. I più usati:
name: l'etichetta mostrata sopra l'anteprima.size: forza le dimensioni del riquadro, utile per simulare uno schermo stretto o una card di dimensioni fisse.textScaleFactor: verifica come si comporta il layout con testi ingranditi per accessibilità.brightness: renderizza in modalità chiara o scura.theme: fornisce unThemeDatapersonalizzato.wrapper: avvolge il widget in un albero arbitrario (provider,Scaffold,Directionality…).localizations: configura delegati e locale per i widget localizzati.
@Preview(
name: 'Card - testo XL',
size: Size(360, 200),
textScaleFactor: 1.8,
brightness: Brightness.dark,
)
Widget productCardLargeText() => productCardAvailable();
Questo tipo di anteprima è particolarmente prezioso: i bug di overflow con textScaleFactor elevato sono tra i più frequenti e tra i più difficili da scoprire durante lo sviluppo normale.
Il wrapper: far funzionare widget che dipendono dal contesto
La maggior parte dei widget reali non vive isolata: ha bisogno di un Scaffold, di un provider di stato, di un repository finto. Il parametro wrapper serve esattamente a questo: riceve il widget della preview e restituisce l'albero in cui inserirlo.
Widget appWrapper(Widget child) {
return ProviderScope(
overrides: [
productRepositoryProvider.overrideWithValue(FakeProductRepository()),
],
child: Scaffold(
backgroundColor: Colors.grey.shade100,
body: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
}
@Preview(name: 'Lista prodotti (dati finti)', wrapper: appWrapper)
Widget productListPreview() => const ProductListView();
La funzione passata a wrapper deve anch'essa essere top-level o statica, perché viene risolta a tempo di compilazione. Lo stesso vale per la funzione indicata in theme.
Un pattern molto comodo è creare un unico file lib/previews/preview_utils.dart con wrapper riutilizzabili: uno "nudo" con solo MaterialApp, uno con i provider finti, uno che simula un tablet.
Preview multiple con MultiPreview
Quando vuoi vedere lo stesso widget in più configurazioni (chiaro/scuro, italiano/inglese, telefono/tablet) puoi dichiarare più annotazioni sulla stessa funzione, oppure definire una classe che raggruppa un insieme di preview riusabili estendendo MultiPreview.
class ThemeModes extends MultiPreview {
const ThemeModes();
@override
// ignore: avoid_field_initializers_in_const_classes
final List<Preview> previews = const <Preview>[
Preview(name: 'Light', brightness: Brightness.light),
Preview(name: 'Dark', brightness: Brightness.dark),
];
}
@ThemeModes()
Widget checkoutButton() => const CheckoutButton(total: 42.50);
Con una sola annotazione ottieni due anteprime affiancate. È la via più rapida per costruire una mini design gallery del design system interno.
Dove mettere le preview
Ci sono due scuole di pensiero:
- Accanto al widget, in fondo allo stesso file. Massima vicinanza al codice, rischio di "sporcare" i file di produzione.
- In file dedicati, ad esempio
lib/previews/product_card_previews.dart. Più ordine, e permette di escludere facilmente l'intera cartella dall'analisi di copertura o da alcune regole di lint.
In entrambi i casi vale una raccomandazione: le preview non devono contenere logica di business né credenziali. Usa dati finti espliciti, costruiti in loco o provenienti da un file fixtures.dart condiviso con i test.
Un vantaggio collaterale: gli stessi oggetti finti che prepari per le preview sono riutilizzabili nei golden test, e viceversa. Le due pratiche si rinforzano a vicenda — la preview serve durante lo sviluppo, il golden test blinda il risultato in CI.
Limiti attuali da conoscere
Prima di adottare il previewer su un progetto grande, tieni presente alcuni punti:
- È una funzionalità in preview: firma dell'annotazione e flag della CLI possono cambiare tra una release e l'altra.
- Il codice nativo non è disponibile: i plugin che dipendono da platform channel (fotocamera, geolocalizzazione, secure storage) non funzionano nel previewer. Vanno sostituiti con fake tramite il
wrapper. - Le chiamate di rete reali vanno evitate: oltre a essere lente, rendono l'anteprima non deterministica. Usa repository finti.
- Non è un sostituto dell'app: flussi di navigazione, deep link e ciclo di vita restano da testare sull'app vera.
- Tempo di avvio: la prima compilazione dello scaffold richiede qualche decina di secondi; gli aggiornamenti successivi sono invece rapidi.
Un flusso di lavoro consigliato
Una routine che funziona bene nella pratica quotidiana:
- Quando crei un nuovo widget di presentazione, scrivi subito due o tre preview: stato normale, stato vuoto/errore, testo lungo.
- Tieni il previewer aperto in un pannello laterale mentre sviluppi la UI.
- Aggiungi una preview con
textScaleFactor: 2.0come controllo di accessibilità. - Quando il widget è stabile, trasforma le preview più significative in golden test.
- Durante le code review, le preview diventano documentazione vivente: chi legge il PR sa quali stati sono stati considerati.
Conclusione
Il Widget Previewer colma uno dei divari storici tra Flutter e i framework dichiarativi nativi. Non rivoluziona il modo di scrivere widget, ma accorcia drasticamente il ciclo di feedback sulla UI, specialmente per design system, componenti riutilizzabili e stati di errore difficili da riprodurre.
Trattandolo come uno strumento complementare — non sostitutivo — a hot reload e golden test, diventa un alleato prezioso: scrivere una preview costa cinque righe e restituisce una verifica visiva immediata, riutilizzabile per sempre.