Ora che abbiamo un quadro più chiaro dei metodi offerti dalla classe StatefulWidget
e dalla classe State
, e dopo aver compreso il ciclo di vita di quest’ultimo, vediamo un esempio pratico di come creare uno stateful widget a partire da uno stateless.
Esempio Pratico
Creiamo un nuovo progetto come illustrato nella lezione 6 di questa guida e cancelliamo tutto eccetto la classe MyApp
, definita come segue.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Lesson 24',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(title: Text('Lesson 24')),
body: Center(child: MyWidget(traveler: "Antonio", country: "Finland")),
)
);
}
}
In particolare possiamo notare che la proprietà body
dello Scaffold
ha come valore il widget MyWidget
, definito come segue.
class MyWidget extends StatelessWidget {
final String traveler;
final String country;
MyWidget({Key key, this.traveler, this.country}): super(key:key);
@override
Widget build(BuildContext context) {
return Text('$traveler visited $country');
}
}
Lo scopo di questo widget è quello di mostrare a schermo un testo contenente una stringa che rappresenta un viaggiatore e la nazione che ha visitato.
Eseguendo l’applicazione, infatti, otterremo il seguente risultato.
Immaginiamo adesso di voler trasformare questo widget in modo che l’utente possa inserire la nazione in modo dinamico modificando il testo gestito dal Text
widget. Per farlo dovremo compiere i seguenti passi:
- estendere la classe
StatefulWidget
, che restituisce un oggettoState
tramite il metodocreateState
. Questa classe inoltre gestirà solo ed esclusivamente la proprietàtraveler
; - creare una classe che estenda la classe
State
, la quale dovrà sovrascrivere il metodoState.build
e gestire la variabilecountry
aggiornabile tramite input dall’utente; - invocare il metodo
State.setState
per modificare lo stato e scatenare una ricostruzione del widget (e del suo sottoalbero).
Partiamo dal primo passo e modifichiamo la classe MyWidget
affinché estenda la classe StatefulWidget
come segue.
class MyWidget extends StatefulWidget {
// . . .
}
Successivamente, rimuoviamo dalla classe la proprietà country aggiornando di conseguenza il costruttore e implementiamo il metodo createState
offerto dalla classe StatefulWidget
e che obbligatoriamente dobbiamo implementare. Al termine delle modifiche otterremo il seguente risultato.
class MyWidget extends StatefulWidget {
final String traveler;
MyWidget({Key key, this.traveler}): super(key:key);
@override
State<StatefulWidget> createState() => _MyWidgetState();
}
In particolare, il metodo createState
restituirà un’istanza della classe privata _MyWidgetState
che definirà lo State
del nostro stateless widget e la UI.
Passiamo quindi al secondo punto e creiamo la classe _MyWidgetState
che dovrà:
- gestire la variabile
country
rimossa dalla classeMyWidget
; - sovrascrivere il metodo
build
per rappresentare la nuova interfaccia composta da due widget: unTextField
per permettere all’utente di inserire un input; unText
che mostri il testo del widget.
Di seguito l’implementazione.
class _MyWidgetState extends State<MyWidget> {
String country='';
@override
Widget build(BuildContext context) {
return
Container(
child:Column(
children: <Widget>[
TextField(
onSubmitted: (String countyName){
country = countyName;
},
),
Divider(),
Text('${widget.traveler} visited $country'),
],
),
width: 200,
height: 200,
);
}
}
In questa classe abbiamo istanziato la proprietà country
ad una stringa vuota, successivamente abbiamo definito un Container
composto a sua volta da un Column
widget, che definisce il TextField
, un Divider
ed il Text
widget.
Più nello specifico abbiamo definito la proprietà onSubmitted
del TextField
affinché ogni volta che l’utente inserisce un input questo venga associato alla proprietà country, mentre per il Text
widget abbiamo effettuato l’accesso al valore delle proprietà:
country
, gestita direttamente dalla classe_MyWidgetState
;widget.traveler
, ossia alla proprietà definita nello stateful widgetMyWidget
a cui possiamo accedere grazie alla proprietàState.widget
che gestisce il riferimento aMyWidget
.
Aggiorniamo quindi la proprietà body dello Scaffold definito nella classe MyApp
come segue.
body: Center(child: MyWidget(traveler: "Antonio")),
Abbiamo quindi passato allo stateful widget MyWidget
solo la proprietà traveler
poichè la country
viene gestita tramite input. Eseguiamo l’applicazione per ottenere il risultato in figura.
Come possiamo notare, nonostante sia stato inserito il testo Finland nel TextField
, l’informazione non viene mostrata nel Text
widget in quanto non è cambiato lo stato dell’oggetto State
associato allo stateful element.
Per risolvere questo inconveniente possiamo passare al terzo punto del nostro elenco, ossia invocare il metodo State.setState
per modificare lo stato e scatenare una ricostruzione del widget.
Per farlo, dobbiamo semplicemente modificare il valore della proprietà TextField.onSubmitted
come segue:
onSubmitted: (String countyName){
setState(() {
country = countyName;
});
},
Così facendo stiamo aggiornando lo stato scatenando la ricreazione del widget come mostrato nella seguente figura.
In questo modo, ogni volta che inseriremo un nuovo testo verrà aggiornato finalmente anche il Text
widget con il nuovo valore inserito.
Dietro le quinte
Schematicamente, quello che accade è rappresentato dalla seguente figura.
Alla creazione del widget nel widget tree, viene richiesta dal framework la creazione di uno stateful element relativo al widget. A questo punto lo stateful element richiede al widget la creazione di un oggetto State
ed è proprio in questo caso che viene invocato il metodo StatefulWidget.createState
, che abbiamo sovrascritto nella classe MyWidget
. Questo metodo quindi crea un nuovo oggetto MyWidgetState
che sarà preso dallo stateful element. A questo punto, il MyWidgetState
invocherà il metodo build
per costruire l’interfaccia e per farlo avrà bisogno proprio delle due proprietà di interesse, ossia traveler
fornita da MyWidget
e country
gestita dall’oggetto _MyWidgetState
. A questo punto viene inserito all’interno del sottoalbero di MyWidget
lo stateless widget Text
contenente la stringa corretta, e nell’element tree verrà montato un nuovo stateless element.
Nel momento in cui l’utente inserirà una nuova nazione visitata, verrà invocato il metodo setState
che aggiorna il valore country
gestito da MyWidgetState
. A questo punto l’oggetto State
marcherà il suo stateful element come dirty
, richiedendo quindi la ricreazione dei suoi widget figli nel frame successivo. Con il frame successivo, lo stateful element invocherà il metodo MyWidgetState.build
per ricostruire il widget e mostrare il nuovo testo attraverso il Text
widget. Il vecchio Text
widget verrà quindi distrutto e sostituito dal nuovo e, proprio perché il nuovo widget ha lo stesso runtimeType
e Widget.key
del precedente, lo stateless element non verrà rimosso e aggiornerà la sua referenza al nuovo widget come mostrato in figura.
Considerazioni
Abbiamo visto un esempio semplice ma efficace per comprendere appieno quello che succede nel ciclo di vita di uno stateful widget. Ciononostante, imparando a conoscere sempre di più questo framework, le occasioni in cui andremo ad utilizzare uno stateful widget saranno sempre di meno, in quanto molti dei principali casi d’uso in cui utilizzarli sono già implementati dal framework stesso. Un esempio è proprio offerto dalla classe StreamBuilder
, che ricostruisce il widget ogni volta che l’oggetto Stream
fornisce un nuovo valore.
Il codice di questa lezione è disponibile su Github.