Introduzione
Uno degli errori più comuni tra chi inizia con Flutter è pensare che nascondere un widget sia una singola operazione. In realtà esistono numerose strategie, ognuna con implicazioni diverse su performance, stato e ricostruzioni. Scegliere quella sbagliata può portare a rebuild inutili, perdita dello stato di scroll o form, oppure a mantenere in memoria widget pesanti che non servono.
In questo articolo vediamo le tecniche principali per il rendering condizionale in Flutter: dalla semplice rimozione dall'albero, a Visibility, Offstage, fino a IndexedStack e al lazy loading vero e proprio.
1. Rimozione dall'albero: il condizionale puro
La forma più diretta è includere o meno il widget nell'albero tramite un if inline o un operatore ternario:
Column(
children: [
const Text('Sempre visibile'),
if (mostraDettagli) const DettagliWidget(),
],
)
Quando mostraDettagli è false, il widget non esiste affatto: non occupa spazio, non consuma memoria e non viene renderizzato. È l'approccio più efficiente in termini di risorse.
Lo svantaggio? Ogni volta che il widget torna visibile viene ricreato da zero, perdendo tutto lo stato interno (posizione di scroll, testo digitato, animazioni in corso).
2. Il widget Visibility
Visibility mantiene il widget nell'albero ma ne controlla la visibilità:
Visibility(
visible: mostraDettagli,
child: const DettagliWidget(),
)
Per impostazione predefinita, quando visible è false il widget viene rimosso dal layout (come Offstage) e non occupa spazio. Possiamo però personalizzarne il comportamento:
Visibility(
visible: mostraDettagli,
maintainState: true, // mantiene lo State del widget
maintainAnimation: true, // continua le animazioni
maintainSize: true, // occupa comunque lo spazio
replacement: const SizedBox.shrink(),
child: const DettagliWidget(),
)
L'opzione maintainState: true è utile quando vogliamo preservare lo stato interno (ad esempio un TextField compilato) senza distruggerlo. Attenzione: mantenere lo stato ha un costo, perché il widget resta vivo in memoria.
3. Offstage: renderizzare senza mostrare
Offstage posiziona il widget "fuori dallo schermo": il widget viene comunque costruito e ne viene calcolato il layout, ma non è dipinto né interattivo.
Offstage(
offstage: !mostraDettagli,
child: const DettagliWidget(),
)
È utile quando serve conoscere le dimensioni di un widget prima di mostrarlo, o quando si vuole precaricarne il layout. Da usare con parsimonia: il layout viene comunque calcolato, quindi non è gratis.
4. IndexedStack: gestire più "pagine" mantenendo lo stato
IndexedStack mostra un solo figlio alla volta in base a un indice, mantenendo però vivi tutti gli altri. È l'ideale per le tab di una BottomNavigationBar quando vogliamo preservare lo stato di ogni schermata.
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _indice = 0;
final _pagine = const [
FeedPage(),
RicercaPage(),
ProfiloPage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _indice,
children: _pagine,
),
bottomNavigationBar: NavigationBar(
selectedIndex: _indice,
onDestinationSelected: (i) => setState(() => _indice = i),
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Feed'),
NavigationDestination(icon: Icon(Icons.search), label: 'Cerca'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profilo'),
],
),
);
}
}
Con IndexedStack la posizione di scroll del feed rimane intatta quando si passa alla ricerca e si torna indietro. Il rovescio della medaglia: tutte le pagine vengono costruite subito, anche quelle mai visitate, con un possibile impatto sull'avvio.
5. Lazy loading: costruire solo al bisogno
Se le pagine sono pesanti, possiamo combinare le tecniche per un caricamento pigro. Un pattern efficace è tenere traccia delle pagine già visitate e costruire solo quelle:
class LazyIndexedStack extends StatefulWidget {
final int index;
final List<Widget> children;
const LazyIndexedStack({
super.key,
required this.index,
required this.children,
});
@override
State<LazyIndexedStack> createState() => _LazyIndexedStackState();
}
class _LazyIndexedStackState extends State<LazyIndexedStack> {
late final List<bool> _attivate =
List.generate(widget.children.length, (i) => i == widget.index);
@override
void didUpdateWidget(LazyIndexedStack oldWidget) {
super.didUpdateWidget(oldWidget);
_attivate[widget.index] = true;
}
@override
Widget build(BuildContext context) {
return IndexedStack(
index: widget.index,
children: List.generate(widget.children.length, (i) {
return _attivate[i]
? widget.children[i]
: const SizedBox.shrink();
}),
);
}
}
In questo modo ogni pagina viene costruita solo la prima volta che viene selezionata, ma una volta attivata resta viva nell'albero e ne conserva lo stato. È il compromesso migliore tra avvio rapido e persistenza dello stato.
Tabella riassuntiva
ifinline: massima efficienza, ma perde lo stato ad ogni riapparizione.Visibility: flessibile, conmaintainStateper preservare lo stato.Offstage: costruisce e calcola il layout ma non dipinge; utile per misurare.IndexedStack: costruisce tutti i figli subito, mantiene sempre lo stato.LazyIndexedStack: costruzione pigra + stato persistente.
Quando usare cosa
- Per elementi UI leggeri che compaiono e scompaiono spesso e non hanno stato importante, usa l'
ifinline. - Per form o widget con stato che devono restare in memoria, opta per
Visibility(maintainState: true). - Per navigazione a tab dove la persistenza dello scroll è fondamentale,
IndexedStack(o la versione lazy se le pagine sono pesanti). - Evita di lasciare in memoria widget molto pesanti (mappe, video, liste enormi) quando non servono: valuta sempre il trade-off tra reattività e consumo di risorse.
Conclusione
Il rendering condizionale in Flutter non è mai una scelta banale. Comprendere la differenza tra rimuovere un widget dall'albero e semplicemente nasconderlo permette di scrivere UI più performanti e con uno stato coerente. La regola d'oro: nascondi solo ciò che vale la pena mantenere vivo, e rimuovi tutto il resto.
