Per motivi di efficienza, il motore di rendering di Flutter determina quali
widget necessitano di essere ridisegnati (re-rendered) e quali no, in questo
modo è possibile mantenere fluida e responsiva l’applicazione.
Per determinare se un widget necessita di essere ridisegnato è necessario sapere
se qualcosa è cambiato dall’ultimo rendering e se quel cambiamento influisce sul
widget. Ovviamente quei widget che non cambiano mai, non dovranno mai essere ridisegnati
e questo può accadere, ad esempio, quando un widget non ha uno stato interno, cioè
si tratta un stateless widget.
Per contro molti dei widget che vengono mostrati dall’applicazione possiedono uno
stato interno che cambia durante l’esecuzione dell’applicazione. Ad esempio una lista
di immagini può cambiare perché nuove immagini vengono caricati, oppure una lista
di messaggi può cambiare perché ne arrivano di nuovi. In Flutter dobbiamo esplicitamente
dichiarare quei widget che possiedono uno stato utilizzando gli stateful widget
In questa lezione capiremo:
- la differenza tra stateful e stateless widget,
- come realizzare semplici
StatefulWidget
.
In questa lezione utilizzeremo l’applicazione Flutter clicker counter che può essere
generata automaticamente usando una IDE (es. VS Code o FlutLab). Tale applicazione
contiene
- un
StatelessWidget
che rappresenta la struttura material design dell’app, - un
StatefulWidget
che rappresenta la parte non fissa dell’app.
Attenzione
Non bisogna confondere un widget con i suoi discendenti (figli, nipoti, …), infatti, mentre
il widget MaterialApp
del nostro esempio è stateless, uno dei suoi discendenti
(quello che tiene il conto dei click) è un stateful widget.
La progettazione Flutter permette al motore di rendering di ridisegnare solo quei
widget che cambiano. I widget che non sono da ridisegnare (ad esempio gli stateless)
non vengono ridisegnati anche se alcuni dei suoi discendenti saranno ridisegnati.
Esercizio
Per meglio comprendere i concetti spiegati in questa lezione, si consiglia di procedere
con la realizzazione di un’applicazione diversa da quella qui presentata. Ad esempio è
possibile usare le API Chuck Norris API per recuperare in
maniera asincrona barzellette casuali a tema Chuck Norris.
Il tipico workflow per creare uno stateful widget prevede l’implementazione di due classi
- una classe che estende
StatefulWidget
- una classe che estende
State<...>
.
Nell’esempio preso in considerazione queste due classi si chiamano MyHomePage
e
_MyHomePageState
rispettivamente. Va sottolineato che la scelta xi rendere la
seconda classe library private (con l’underscore iniziale) non è obbligata, ma
solo consigliata.
La classe MyHomePage
contiene semplicemente il titolo e il metodo createState()
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({Key? key, required this.title}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
La sottoclasse di State
La classe State
è
la classe Flutter che si occupa di gestire lo stato di un widget. Lo stato va gestito
nel senso che è necessario tenere traccia di eventuali suoi cambiamenti.
Per utilizzare la class State
, questa va estesa specializzandolo con il
StatefulWidget
di cui rappresenta lo stato. All’interno della classe ottenuta
verranno, inoltre, memorizzate le variabili di classe che rappresentano lo stato.
Nel nostro esempio, la dichiarazione di questa classe sarà la seguente
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
// ...
}
Si noti che nella dichiarazione della classe abbiamo aggiunto la variabile di istanza
_counter
(rendendola library private) che sarà il nostro stato (il conteggio del
numero di volte che il pulsate è stato cliccato).
Tale dichiarazione, tuttavia, genera un errore di compilazione se non si provvede
ad implementare (@override
) il metodo build
che ha il compito di restituire un
Widget
che verrà costruito sulla base dello stato interno.
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
// ...
@override
Widget build(BuildContext context) {
// ...
}
}
Aggiornamento dello stato
Per aggiornare lo stato in una classe che estende State
bisogna utilizzare il
metodo setState
della classe State
stessa. La sintassi di setState
prevede
che venga passato come parametro uan funzione che cambia lo stato (cioè l variabili
di istanza). Nel nostro esempio lo stato verrà cambiato ad ogni click e questo
cambiamento sarà un incremento _counter++
. Il modo corretto per realizzare questo
incremento è attraverso il metodo setState
, normalmente si usa un metodo wrapper
all’interno del quale si chiama setState
.
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
// ...
}
}
Attenzione
È fondamentale che l’aggiornamento dello stato avvenga utilizzando il metodo
setState
, infatti Flutter utilizza questo metodo per aggiornare l’interfaccia,
se necessario.
In certi casi può accadere che l’interfaccia si aggiorni anche senza l’uso di
setState
, se questo accade, tuttavia, è probabile che sia stato un qualche altro
evento a richiedere il ridisegno dell’interfaccia e che, per puro caso, si sia
osservato anche il refresh dello stato.
La parte di _MyHomePageState
che ci rimane da discutere è il corpo del metodo build
il quale contiene una sola istruzione return
che restituisce il widget da visualizzare.
Questo widget interagisce con lo stato in due modi
- accedendo alla variabile
_counter
per leggere lo stato e - invocando il metodo
_incrementCounter
per aggiornare lo stato.
Il rimanente codice crea un widget di tipo Scaffold
che contiene il layout
dell’applicazione, il risultato è la seguente classe _MyHomePageState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:',),
Text('$_counter',style: TextStyle(fontSize: 25),),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add),
),
);
}
}
|
Si noti l’utilizzo dello stato:
- alla riga
19
la variabile _counter
viene usata all’interno di un Text
, - alla riga
24
il metodo _incrementCounter
viene usato come callback un
FloatingActionButton
.
La parte stateless dell’applicazione
Sopra ci siamo occupati della creazione di uno StatefulWidget
per mantenere
come stato il numero di volte che l’utente ha cliccato sul bottone. Nella versione
completa della App il root widget è di tipo stateless e si limita a creare tutto
il necessario per l’applicazione material design avente come contenuto il widget
stateful sopra discusso.
Di seguito il codice contenente il main
e il root widget (stateless).
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Stateful Clicker Counter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// di seguito viene creata l'istanza dello stateful widget
home: MyHomePage(title: 'Flutter Demo Clicker Counter Home Page'),
);
}
}
Ogni widget possiede un ciclo di vita (lifecycle) che descrive in che stato
si trova il widget durante l’esecuzione della App. Vediamo brevemente il ciclo
di vita di un StatefulWidget
.
Di seguito vediamo quali metodi sono disponibili in un StatefulWidget
e quando
questi vengono invocati, ognuno di questi metodi può essere ridefinito se
necessario (es. initState()
)
createState()
: per prima cosa lo stato deve essere creatoinitState()
: dopo la creazione lo stato deve essere inizializzatodidUpdateWidget()
: c’è stato un update nell’interfaccia che necessita di un ridisegnare il widgetsetState()
: lo stato viene esplicitamente cambiato per cui è necessario ridisegnare il widget.build()
: il widget verrà ridisegnato, build()
viene chiamato dopo initState()
,
setState()
e didUpdateWidget()
Esistono anche metodi che vengono invocati quando lo stato sta per essere eliminato
deactivate()
: il widget non è più visualizzato, ma potrebbe tornare visibile in futurodispose()
: il widget sta per essere distrutto definitivamente
Non sempre sempre serve ridefinire gli eventi sopra elencati, tuttavia in alcuni
casi questo potrebbe risultare necessario.
initState()
può risultare utile inizialmente, quando lo stato deve essere caricato
e/o impostato a qualche valore di default.deactivate()
può essere usato quando sia necessario mettere in pausa qualche
operazione e/o salvare lo stato.dispose()
può essere utilizzato quando vi sia qualche risorsa che deve essere
rilasciata prima che il widget venga definitivamente eliminato (lo stato andrebbe
salvato utilizzando l’evento deactivate()
).