Flutter Hooks, un "nuovo" modo per gestire il life-cycle dei widget

Picasso affermava saggiamente: I buoni artisti copiano, i grandi artisti rubano. Oppure, potrebbe essere stato Steve Jobs; in ogni caso, questo approccio è un fenomeno frequente nel mondo della programmazione.

Chi ha avuto esperienza con Flutter comprende appieno quanto possa risultare tedioso lavorare con gli StatefulWidget. Sebbene gli StatefulWidget siano una soluzione eccellente per ottenere widget dinamici, presentano anche una serie di svantaggi da considerare attentamente. Questi includono la necessità di invocare il metodo setState() ogni volta che se ne ha bisogno e l'obbligo per lo sviluppatore di scrivere codice boilerplate poiché vi è difficoltà nel riciclare le logiche scritte all'interno dei vari metodi del ciclo di vita, come initState() e dispose().

Pertanto, in linea con l'approccio adottato da React, la ragione alla base di Flutter Hooks è chiara: gestire il ciclo di vita dei widget e incrementare la condivisione del codice tra i diversi widget presenti in un'applicazione Flutter, riducendo nel frattempo la presenza di codice duplicato.

 

Cosa sono gli Hook?

Gli Hooks sono degli oggetti nati su React e introdotti su Flutter per semplificare la gestione del ciclo di vita dei widget e dello stato. Questi oggetti, simili a state, sono memorizzati all'interno di un Element, ma a differenza di avere uno stato singolo, l'Element contiene una lista di hooks. Questo approccio consente una maggiore flessibilità e chiarezza nella gestione dello stato e del ciclo di vita dei widget.

Per utilizzare gli Hooks, anziché utilizzare i classici StatelessWidget e StatefulWidget, dobbiamo utilizzare HookWidget. All’interno del metodo build() possiamo inserire gli Hooks di cui abbiamo bisogno. Oltre a permetterci di utilizzare gli Hooks, HookWidget sovrascrive il comportamento di default dell’hot-reload, affinché gli hooks non vengano resettati. Inoltre, non dobbiamo preoccuparci del dispose() dei vari hook che utilizziamo, dato che viene gestito autonomamente dagli hook stessi.

 

1class MyCoolWidget extends HookWidget {
2	const MyCoolWidget({Key? key}) : super(key: key);
3	
4	Widget build(BuildContext context) {
5		useCoolHook();
6		return const SizedBox();
7	}
8}

 

Come scrivere un Hook

Esistono due modi per scrivere un Hook. Vediamoli di seguito.

 

Creare un Hook come Funzione

Le funzioni sono la via più comune per scrivere un hook. Gli Hooks sono componibili, di conseguenza, una funzione può combinare più hook al suo interno,permettendo la creazione di hook più complessi. Queste funzioni, per convenzione, utilizzano il prefisso use.

1bool useIsDarkMode() {
2  final result = usePlatformBrightness();
3
4  return result == Brightness.dark;
5}

 

Creare un Hook come una Classe

Quando un hook diventa troppo complesso, è possibile convertirlo in una classe che eredita da Hook. Come classe, gli hook sono molto simili alla classe State e hanno accesso ai metodi di life-cycle del widget, come initHook, dispose e setState.

1Brightness? usePlatformBrightness() {
2  return use(_DetectBrightness());
3}
4
5class _DetectBrightness extends Hook<Brightness?> {
6  
7  HookState<Brightness?, Hook<Brightness?>> createState() {
8    return _DetectBrightnessState();
9  }
10}
11
12class _DetectBrightnessState extends HookState<Brightness?, _DetectBrightness>
13    with WidgetsBindingObserver {
14  Brightness? brightness;
15
16  _DetectBrightnessState() {
17    brightness =
18        SchedulerBinding.instance.platformDispatcher.platformBrightness;
19  }
20
21  
22  void initHook() {
23    WidgetsBinding.instance.addObserver(this);
24    super.initHook();
25  }
26
27  
28  void dispose() {
29    WidgetsBinding.instance.removeObserver(this);
30    super.dispose();
31  }
32
33  
34  Brightness? build(BuildContext context) => brightness;
35
36  
37  void didChangePlatformBrightness() {
38    setState(() {
39      brightness =
40          SchedulerBinding.instance.platformDispatcher.platformBrightness;
41    });
42    super.didChangePlatformBrightness();
43  }
44}

 

Hook Preesistenti

 

useState()

Questo hook consente ai widget di gestire l’ephemeral state di un Widget, in modo più conciso rispetto a un StatefulWidget tradizionale.

1final counter = useState(0)

 

useCallback()

Per evitare la creazione di nuove funzioni ogni volta che avviene il re-render di un widget, ottimizzando le prestazioni.

1final incrementCounter = useCallback(() => counter.value++, []);  

 

useValueChanged()

Utile per monitorare un valore e attivare una callback ogni volta che il suo valore cambia. È particolarmente utile in scenari in cui si desidera reagire dinamicamente a cambiamenti specifici di un dato.

1useValueChanged<int, void>(counter.value, (oldValue, _) {
2	isCounterChanged = counter.value != oldValue;
3});

 

useEffect()

Hook indicato per l'esecuzione e il controllo di side-effects. Questo metodo, oltre ad effettuare automaticamente il dispose(), viene eseguito ogni qual volta che keys varia, se specificato.

1useEffect(() {
2	if (isCounterChanged) {
3		print("Value updated");
4	}
5	return;
6}, [isCounterChanged]);

 

useMemoized()

Questo Hook effettua la memorizzazione di una funzione, ovvero salva in memoria i valori restituiti da una funzione in modo da averli a disposizione per un riutilizzo successivo senza doverli ricalcolare. Immaginiamo di avere un algoritmo ad alta potenza computazionale come il crivello di Erastotene. Utilizzando quest’hook, possiamo effettuare il caching del risultato di quest’algoritmo.

1final primeNumbers = useMemoized(() => sieveOfEratosthenes(1000));

 

useRef()

È utilizzato per creare un oggetto che contiene una singola proprietà mutabile. A differenza delle variabili locali, il valore di useRef persiste attraverso ri-renderizzazioni del widget senza innescare un nuovo render. Questo rende useRef utile per mantenere riferimenti a oggetti che devono sopravvivere attraverso diversi cicli di rendering.

1final textController = useRef(TextEditingController());
2
3TextField(
4  controller: textController.current,
5  // Altri attributi del TextField
6);

 

useContext()

L'hook useContext è utilizzato per ottenere il BuildContext associato a un HookWidget durante la costruzione del widget. Questo è particolarmente utile quando si desidera accedere al contesto senza dover passare esplicitamente il contesto come argomento a vari metodi.

1final BuildContext context = useContext();

 

useAnimation() e useAnimationController()

useAnimation() è un hook che semplifica l'utilizzo di Animation mentre useAnimationController() è un hook per gestire le animazioni dei widget, che riduce drasticamente la scrittura di codice, evitando di creare uno StatefulWidget con uno stato che implementa il mixin SingleTickerProviderStateMixin.

1class FavoriteButton extends HookWidget {
2  const FavoriteButton({super.key});
3
4  
5  Widget build(BuildContext context) {
6    final animator = useAnimationController(
7      duration: const Duration(milliseconds: 300)
8    );
9
10    final animation = useAnimation(
11      ColorTween(
12        begin: Colors.white,
13        end: Colors.pink.shade300,
14      ).animate(animator),
15    );
16
17    final isFav = useState(false);
18
19    return IconButton(
20      padding: const EdgeInsets.all(16),
21      color: Colors.white,
22      icon: isFav.value
23          ? Icon(Icons.favorite, color: animation)
24          : Icon(Icons.favorite_border, color: animation),
25      onPressed: () {
26        isFav.value = !isFav.value;
27      },
28    );
29  }
30}

 

useScrollController()

Hook che restituisce e gestisce uno ScrollController.

1class MyCoolList extends HookWidget {
2  const MyCoolList({super.key});
3
4  
5  Widget build(BuildContext context) {
6    final ScrollController scrollController = useScrollController();
7
8    return Scaffold(
9      appBar: AppBar(
10        title: const Text('My Cool List'),
11      ),
12      body: ListView.builder(
13        controller: scrollController,
14        itemCount: 50,
15        itemBuilder: (context, index) {
16          return ListTile(
17            title: Text('Item $index'),
18          );
19        },
20      ),
21      floatingActionButton: FloatingActionButton(
22        onPressed: () {
23          // Scroll to the top when the FloatingActionButton is pressed
24          scrollController.animateTo(
25            0.0,
26            duration: const Duration(milliseconds: 500),
27            curve: Curves.easeInOut,
28          );
29        },
30        child: const Icon(Icons.arrow_upward),
31      ),
32    );
33  }
34}

 

In conclusione

Come abbiamo visto, l'utilizzo di Flutter Hooks semplifica notevolmente la gestione del life-cycle dei widget in Flutter, in più riduce i tempi di sviluppo e di review, rendendo la struttura del codice più lineare e ben strutturata.

Autore

Stefano Liuzzo Scorpo

Appassionato di tecnologia, informatica, videogiochi, e di tutto quello che fa Beep Boop, smonta, monta e ripara (e rompe) oggetti sin dal 1997. Fiero proprietario di due stampanti 3D, un Commodore 64 e svariati monitor CRT, è uno sviluppatore Frontend.

Utilizza principalmente con Flutter, lo ama così tanto da cercare di convertire anche i Reactiani più fondamentalisti all’interno di Devmy. Si occupa anche di QA (forse perché è bravo a rompere tutto quello che smonta?). I suoi giochi preferiti, Super Metroid e Chrono Trigger e il suo sogno è possedere una Lamborghini Miura SV.

Devmy su linkedin

Contattaci.

Hai un progetto in mente?

Anche se - semplicemente - vuoi prendere un caffè con noi o vedere la nostra collezione di Action Figures scrivici tramite questo form.

Questo sito è protetto da reCAPTCHA e si applicano le Norme sulla privacy e i Termini di servizio di Google.

Ultimi Articoli