Nelle lezioni precedenti, abbiamo introdotto alcune delle nozioni fondamentali relative al linguaggio di programmazione Dart e ai principali dettagli tecnici del framework, focalizzando l’attenzione sul processo di rendering dei widget (elemento principe in Flutter). In particolare, come già anticipato nella lezione 7, all’interno di un’app Flutter quasi tutto è un widget, inclusi l’allineamento, i testi e i layout. In particolare esistono due macro classi di widget, che sono gli Stateful e Stateless Widget.
Entrambe le classi si differenziano in un unico aspetto: l’abilità di ricaricare il widget a runtime.
Questa differenza gioca un ruolo di grande importanza nella creazione di un’applicazione interattiva e in questa lezione approfondiremo queste due classi di widget per meglio comprendere quando utilizzarle.
Tipi di Layout
Durante la creazione del layout di un’app ci troviamo a dover classificare i suoi componenti in due categorie:
Categoria | Descrizione |
---|---|
layout statico | è quel tipo di layout che non cambia una volta che è stato costruito e nessuna azione o evento generato dall’utente potrà modificare gli elementi che lo compongono |
layout dinamico | contrariamente al precedente, questo tipo di layout si modifica in base alle attività e agli eventi legati agli utenti. Ad esempio, in un’app di cucina l’utente che aggiunge ai preferiti una ricetta genererà un evento che modificherà l’icona dei preferiti da vuota a piena. Questa azione sottolinea che l’elemento in questione è cambiato a causa di un’azione compiuta dall’utente |
Per venire incontro all’esigenza di dover modellare queste due tipologie di layout, sono state definite in Flutter due tipologie di widget, Stateful e Stateless Widget, utilizzate rispettivamente per il layout dinamico e statico.
Stateless Widget
Nella creazione di un’app, spendiamo una buona parte del nostro tempo a dover creare elementi visivi, in questo caso widget, che dipendono dalle configurazioni definite al momento della creazione da parte del nodo padre e dal BuildContext
che ne gestirà la posizione nel widget tree. Infatti, un context (contesto) in Flutter è semplicemente un riferimento alla posizione del widget all’interno del widget tree e può appartenere ad un solo widget.
Una volta creati, gli stateless widget non subiranno alcuna variazione e non modificheranno il proprio comportamento nemmeno in base a eventi o azioni dell’utente.
Questa tipologia di widget è conosciuta col nome di stateless widget, e alcuni esempi tipici appartenenti a questa categoria sono Text
, Row
, Column
, e Container
.
Per creare uno stateless widget deve essere estesa la classe StatelessWidget
, che richiede la sovrascrittura del metodo build()
per definire la porzione di interfaccia utente rappresentata dal widget in questione.
Il metodo build()
viene tipicamente invocato:
- la prima volta che il widget viene inserito nel widget tree in uno specifico
BuildContext
; - quando le dipendenze del widget cambiano.
L’implementazione di questo metodo deve dipendere solo da:
- i campi del widget, che non devono cambiare nel tempo;
- qualsiasi stato ottenuto dal contesto attraverso il metodo
inheritFromWidgetOfExactType
diBuildContex
.
In mancanza di tali condizioni, è raccomandato utilizzare al posto di uno StatelessWidget
uno StatefulWidget
.
Da ciò si evince che le proprietà di uno StatelessWidget
sono immutabili e l’unico modo per modificarle è la creazione di una nuova istanza di quel widget.
Creiamo, ora, il nostro primo stateless widget estendendo la classe StatelessWidget
e sovrascrivendo il metodo build()
come segue.
class OrangeContainer extends StatelessWidget {
const OrangeContainer({ Key key, this.text}) : super(key: key);
final String text;
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text(this.text),
),
body: Center(
child: Container(
color: const Color(0xFFFF7F50),
constraints: BoxConstraints(
maxHeight: 300.0,
maxWidth: 200.0,
minWidth: 150.0,
minHeight: 150.0
),
)
)
);
}
}
In questo esempio abbiamo creato una nuova classe chiamata OrangeContainer
che definisce al suo interno:
- il costruttore della classe per inizializzare un identificatore per il widget, ossia la
Key
, e il parametrotext
, che altro non è se non una stringa da utilizzare per l’AppBar
; - un widget
Scaffold
che implementa una struttura del layout per il material design e ci permette di definire le proprietà appBar e body utilizzate rispettivamente per impostare un widgetAppBar
e il contenuto delloScaffold
; - l’
AppBar
che mostrerà il testo passato come parametro al momento dell’inizializzazione dello stateless widget; - il
Container
, posto al centro della schermata tramite il widgetCenter
, che definisce al suo interno due proprietà,color
econstraints
.
Per richiamare questo oggetto basterà semplicemente inizializzare l’oggetto all’interno della nostra app. Immaginando di partire da un progetto appena creato, basterà cambiare all’interno della classe MyApp
il valore del parametro home come segue
class MyApp extends StatelessWidget {
. . .
home: OrangeContainer(text: 'Lesson 10')
. . .
}
Il ciclo di vita di uno StatelessWidget
è quindi abbastanza semplice in quanto richiede:
- l’inizializzazione del widget;
- l’invocazione del metodo
build()
per crearlo.
Eseguendo l’app, otterremo il seguente risultato.
Figura 54. Visualizzazione dello stateless widget OrangeContainer per a) Android e b) iOS (click per ingrandire)
Stateful Widget
Compreso cosa sono gli stateless widget, diventa alquanto semplice definire gli stateful widget. Infatti, contrariamente ai primi, gli stateless widget sono dinamici: permettono di cambiare il loro contenuto nel tempo in base alle azioni o agli eventi scaturiti dall’utente e non sono relativi a un qualche stato immutabile passato all’inizializzazione dell’oggetto. I widget Stateful sono pertanto utili quando la porzione dell'interfaccia utente che si sta realizzando può cambiare in modo dinamico.
Come per gli stateless widget, gli stateful widget vengono creati estendendo la classe StatefulWidget
che permette di rendere dinamico lo stato del widget. Questa tipologia di widget è, infatti, caratterizzata dal widget State
che contiene i dati del widget e:
- può essere letto in modo sincrono quando il widget viene creato;
- potrebbe cambiare durante il ciclo di vita del widget stesso.
Lo State
quindi gestisce le informazioni per interagire con il widget in termini di comportamento e layout. Pertanto, ogni volta che avverrà un cambiamento nello State
ciò forzerà un ricreazione del widget al fine di apportare le modifiche, rendendole visibile all’utente finale.
Contrariamente agli StatelessWidget
, negli StatefulWidget
è presente una relazione tra lo State
e il BuildContext
. Infatti, uno State
è sempre associato in modo permanente ad un BuildContext
e non lo cambierà mai. Quando avviene questa associazione, lo State
viene considerato come mounted
.
Alcuni esempi di stateful widget sono Image
, Form
, e Checkbox
.
Diversamente dallo StatelessWidget
, lo StatefulWidget
non dispone del metodo build()
per costruire il widget, ma del metodo createState()
che crea uno stato mutabile per il widget in base alla posizione del widget tree. Le classi che quindi estendono StatefulWidget
devono sovrascrivere questo metodo che ritornerà una nuova istanza della sottoclasse dello stato associato:
_MyState createState() => _MyState();
Il framework può richiamare questo metodo diverse volte durante tutto il ciclo di vita di uno StatefulWidget
. Inoltre, se il widget viene inserito in diverse posizioni nell’albero dei widget, il framework creerà istanze separate dell’oggetto State
per ogni posizione. Qualora, invece, il widget dovesse essere rimosso e reinserito nell’albero, verrà creata una nuova istanza di State
.
Creiamo, ora, il nostro primo stateless widget estendendo la classe StatefulWidget
e sovrascrivendo il metodo createState()
. In questo esempio vogliamo creare un semplice contatore dove ogni volta che l’utente cliccherà sull’icona di +
verrà incrementato il contatore.
Iniziamo col definire la classe Counter
che estende lo StatefulWidget
e sovrascrive il metodo createState()
.
class Counter extends StatefulWidget {
Counter({Key key}) : super(key: key);
@override
_CounterState createState() => _CounterState();
}
Nel metodo createState()
stiamo ritornando una classe privata _CounterState()
che estende a sua volta la classe State
, di cui si è discusso in precedenza, ed è implementata come segue.
class _CounterState extends State<Counter> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('StatefulWidget'),
),
body: Center(
child: Text(
'$_counter',
style: TextStyle(fontSize: 30),
)
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(Icons.add),
),
);
}
}
All’interno della classe è:
- definita una variabile privata
_counter
che è impostata a zero; - implementato un metodo per incrementare il contatore, il quale a sua volta invoca internamente il metodo
setState()
che notifica al framework che lo stato dell’oggetto è cambiato; - sovrascritto il metodo
build()
della classeState
che definisce l’interfaccia utente rappresentata da questo widget.
Diversamente dal metodo build()
della classe StatelessWidget
, il metodo build()
di State
viene invocato dal framework in diverse situazioni:
- dopo l’invocazione del metodo
initState
, ossia quando lo State è inserito nell’albero; - dopo l’invocazione del metodo
didUpdateWidget
, ossia quando cambiano le configurazioni del widget; - dopo la ricezione di una chiamata a
setState
; - dopo che la dipendenza dello
State
è cambiata; - dopo la deattivazione e il reinserimento dello State associato al widget nel widget tree ma in una posizione differente dalla precedente.
Questi eventi rappresentano, inoltre, i momenti salienti del ciclo di vita dello State
associato allo StatefulWidget
.
Eseguendo l’applicazione otterremo il seguente risultato.
Ogni volta che l’utente cliccherà sull’icona di aggiunta, verrà incrementato il contatore che aggiornerà lo State
associato al widget notificandolo al framework, il quale scatenerà nuovamente l’invocazione del metodo build()
di _CounterState
. Pertanto, come precedentemente sottolineato, ogni volta che l’azione dell’utente genera un cambiamento dello stato di un Widget è consigliato utilizzare uno StatefulWidget
.
Stateless o stateful widget
Comprese quindi le caratteristiche di queste due categorie di widget, la domanda che sorge spontanea è:
“Ho bisogno creare un Widget Stateless o Stateful?”
Per rispondere però a questa domanda bisogna comprendere se nel ciclo di vita del widget da creare è utile considerare o meno una variabile che può cambiare nel tempo e una volta modificata costringerà il widget ad essere ricreato. Nel caso in cui, dovesse essere necessario utilizzare una variabile di questo tipo allora il widget da realizzare sarà di tipo stateful, contrariamente sarà stateless.
Vediamo alcuni esempi che possono chiarire la scelta.
Supponiamo di avere un widget che mostra all’utente finale una lista di checkbox come può essere ad esempio una lista della spesa. Ognuna di queste voci sarà quindi rappresentata attraverso un oggetto avente un titolo e uno stato che viene modificato quando l’utente cliccherà sulla checkbox. In questo caso, avremo bisogno di uno StatefulWidget
al fine di tenere traccia degli stati di tutti gli oggetti nella lista e ridisegnare correttamente le checkbox.
Contrariamente, immaginiamo di avere una schermata in cui viene visualizzata una lista di ricette dove ogni elemento della lista è un widget composto da un titolo, una difficoltà e tipologia. A questo widget verrà passato solo un riferimento della ricetta che sarà usato solo per popolare il contenuto del widget, che non salverà alcuno stato poichè sta usando i dati passati dal nodo padre. In questo caso è logico usare uno StatelessWidget
.