Salta al contenuto principale

Informazioni sugli hook

Questa pagina spiega cosa sono gli hooks e come si relazionano con Riverpod.

Gli "Hooks" sono utilità comuni da un pacchetto separato, indipendente da Riverpod: flutter_hooks. Sebbene flutter_hooks sia completamente un pacchetto separato e non ha nulla a che fare con Riverpod (almeno direttamente), è comune usarlo in coppia con Riverpod.

Dovresti utilizzare gli hook?

Gli hook sono uno strumento potente, ma non sono per tutti. Se sei un nuovo arrivato su Riverpod, probabilmente dovresti evitare di utilizzarli.

Nonostante siano utili, gli hook non sono necessari per Riverpod. Non dovresti cominciare ad usare gli hook solo per Riverpod. Piuttosto, dovresti iniziare ad utilizzarli perché vuoi utilizzare gli hook.

Usare gli hook è un compromesso. Possono essere ottimi per produrre codice robusto e riutilizzabile, ma sono anche un nuovo concetto da imparare, e all'inizio possono creare confusione. Gli hooks non sono un concetto fondamentale di Flutter. In quanto tali, si sentiranno fuori posto in Flutter/Dart.

Cosa sono gli hooks?

Gli hook sono funzioni utilizzate dentro i widget. Sono progettati come un alternativa agli StatefulWidget, per rendere la logica più robusta e componibile.

Gli hook sono un concetto che arriva da React, e flutter_hooks è semplicemente un porting dell'implementazione React su Flutter. Pertanto, possono sembrare un po' fuori posto in Flutter. Idealmente, in futuro avremmo una soluzione al problema risolto dagli hook, progettata appositamente per Flutter.

Se i provider di Riverpod sono per lo stato "globale" dell'applicazione, gli hook sono per lo stato locale dei widget. Sono tipicamente utilizzati per interagire con gli oggetti dell'UI aventi stato, come TextEditingController, AnimationController. Possono anche servire come sostituzione al pattern "builder", sostituendo widget come FutureBuilder/TweenAnimatedBuilder con un'alternativa che non prevede la "nidificazione", migliorando drasticamente la leggibilità.

In generale, gli hook sono utili per:

  • form
  • animazioni
  • reagire agli eventi dell'utente
  • ...

Come esempio, possiamo utilizzare gli hook per implementare manualmente una animazione fade-in, dove un widget inizialmente è invisibile per poi apparire.

Se dovessimo utilizzare StatefulWidget, il codice dovrebbe essere il seguente:

class FadeIn extends StatefulWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);

final Widget child;


State<FadeIn> createState() => _FadeInState();
}

class _FadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
late final AnimationController animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);


void initState() {
super.initState();
animationController.forward();
}


void dispose() {
animationController.dispose();
super.dispose();
}


Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
builder: (context, child) {
return Opacity(
opacity: animationController.value,
child: widget.child,
);
},
);
}
}

Utilizzando gli hook, l'equivalente sarebbe:

class FadeIn extends HookWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);

final Widget child;


Widget build(BuildContext context) {
// Crea un AnimationController. Il controller sarà automaticamente distrutto
// quando il widget verrà smontato.
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);

// useEffect è l'equivalente di initState + didUpdateWidget + dispose.
// La callback passata all'useEffect è eseguita la prima volta che l'hook è invocato,
// e poi ogni volta che la lista passata come secondo parametro cambia.
// Siccome in questo caso passiamo una lista const vuota, è strettamente equivalente a "initState".
useEffect(() {
// inizia l'animazione quando il widget viene renderizzato per la prima volta.
animationController.forward();
// Potremmo opzionalmente restituire della logica "dispose" qui
return null;
}, const []);

// Informa Flutter di ricostruire questo widget quando l'animazione si aggiorna.
// Questo è equivalente ad AnimatedBuilder
useAnimation(animationController);

return Opacity(
opacity: animationController.value,
child: child,
);
}
}

Ci sono alcuni aspetti interessanti da notare in questo codice:

  • Non c'è nessuna memory leak. Questo codice non ricrea un nuovo AnimationController ogni volta che il widget si ricostruisce, e il controller è correttamente smaltito quando il widget viene smontato.

  • È possibile utilizzare gli hook quante volte vogliamo all'interno dello stesso widget. Perciò possiamo creare multipli AnimationController se vogliamo:


    Widget build(BuildContext context) {
    final animationController = useAnimationController(
    duration: const Duration(seconds: 2),
    );
    final anotherController = useAnimationController(
    duration: const Duration(seconds: 2),
    );

    ...
    }

    Questo ricrea due controller, senza nessuna sorta di conseguenza negativa.

  • Se vogliamo, possiamo riscrivere questa logica in una riusabile funzione separata:

    double useFadeIn() {
    final animationController = useAnimationController(
    duration: const Duration(seconds: 2),
    );
    useEffect(() {
    animationController.forward();
    return null;
    }, const []);
    useAnimation(animationController);
    return animationController.value;
    }

    Potremmo poi utilizzare questa funzione all'interno dei nostri widget, purché quel widget sia un HookWidget:

    class FadeIn extends HookWidget {
    const FadeIn({Key? key, required this.child}) : super(key: key);

    final Widget child;


    Widget build(BuildContext context) {
    final fade = useFadeIn();

    return Opacity(opacity: fade, child: child);
    }
    }

    Nota come la nostra funzione useFadeIn è completamente indipendente dal nostro widget FadeIn. Se volessimo, potremmo usare la funzione useFadeIn in un widget completamente diverso e funzionerebbe comunque!

Le regole degli hook

Gli hook sono dotati di vincoli unici:

  • Possono solo essere utilizzati all'interno del metodo build di un widget che estende HookWidget:

    Giusto:

    class Example extends HookWidget {

    Widget build(BuildContext context) {
    final controller = useAnimationController();
    ...
    }
    }

    Sbagliato:

    // Non un HookWidget
    class Example extends StatelessWidget {

    Widget build(BuildContext context) {
    final controller = useAnimationController();
    ...
    }
    }

    Sbagliato:

    class Example extends HookWidget {

    Widget build(BuildContext context) {
    return ElevatedButton(
    onPressed: () {
    // Non è realmente dentro il metodo "build", ma è dentro
    // una funzione che interagisce con l'utente (in questo caso "onPressed")
    final controller = useAnimationController();
    },
    child: Text('click me'),
    );
    }
    }
  • Non possono essere utilizzati condizionalmente in un loop.

    Sbagliato:

    class Example extends HookWidget {
    const Example({required this.condition, super.key});
    final bool condition;

    Widget build(BuildContext context) {
    if (condition) {
    // Gli hook non dovrebbero essere usati dentro "if"/"for" ...
    final controller = useAnimationController();
    }
    ...
    }
    }

Per maggiori informazioni riguardo agli hook, vedere flutter_hooks.

Hooks e Riverpod

Installazione

Dato che gli hook sono indipendenti da Riverpod, è necessario installare gli hook separatamente. Se vuoi utilizzarli, installare hooks_riverpod non basta. Avrai comunque bisogno di aggiungere flutter_hooks alle tue dipendenze. Consulta Introduzione for maggiori informazioni.

Utilizzo

In alcuni casi, vorresti scrivere un Widget che utilizza sia gli hook che Riverpod. Ma come avresti già potuto notare, sia gli hook che Riverpod forniscono il proprio tipo base di widget personalizzato: HookWidget e ConsumerWidget. Ma le classi possono solo estendere una superclasse alla volta.

Per risolvere questo problema, puoi utilizzare il pacchetto hooks_riverpod. Questo pacchetto fornisce una classe HookConsumerWidget che combina sia HookWidget che ConsumerWidget in un singolo tipo. Puoi quindi sottoclassare HookConsumerWidget invece di HookWidget:


// Estendiamo HookConsumerWidget invece di HookWidget
class Example extends HookConsumerWidget {

Widget build(BuildContext context, WidgetRef ref) {
// Possiamo utilizzare sia gli hook che i provider qui
final counter = useState(0);
final value = ref.watch(myProvider);

return Text('Hello $counter $value');
}
}

In alternativa, puoi utilizzare i "builder" forniti da entrambi i pacchetti. Per esempio, potremmo continuare ad utilizzare StatelessWidget, ed usare sia HookBuilder che Consumer.


class Example extends StatelessWidget {

Widget build(BuildContext context) {
// Possiamo utilizzare i builder forniti da entrambi i pacchetti
return Consumer(
builder: (context, ref, child) {
return HookBuilder(builder: (context) {
final counter = useState(0);
final value = ref.watch(myProvider);

return Text('Hello $counter $value');
});
},
);
}
}
note

Questo approccio funzionerebbe senza utilizzare hooks_riverpod. Solo flutter_riverpod è necessario.

Se ti piace questo approccio, hooks_riverpod lo semplifica fornendo HookConsumer, che è una combinazione di entrambi i builder in uno:


class Example extends StatelessWidget {

Widget build(BuildContext context) {
// L'equivalente di usare Consumer e HookBuilder insieme.
return HookConsumer(
builder: (context, ref, child) {
final counter = useState(0);
final value = ref.watch(myProvider);

return Text('Hello $counter $value');
},
);
}
}