Tutorial Flutter hooks
Dans ce post, nous allons utiliser les flutter hooks dans une application.
La logique implémentée dans nos widgets peut être difficile à réutiliser, et des fois nous rajoutons beaucoup de complexité dans les méthodes liées au cycle de vie de notre widget , comme le initState()
. En utilisant les hooks, nous avons utilisé un mécanisme nous permettant de séparer la logique UI en plusieurs composants indépendants appelés hooks. Ce qui aura pour objectif de rendre le code claire, réutilisable mais surtout maintenable.
L’utilisation du plugin Flutter Hooks nous permettra d’implémenter la mécanisme de hooks dans notre application Flutter.
Notre application tutorial
Nous allons implémenter une application qui masque la FloatingActionButton
lorsque l’utilisateur scrolle une ListView
. Pour vous guider dans ce post, nous allons analyser une implémentation de l’application sans l’utilisation des hooks.
A. Implémentation sans la librairie flutter_hooks
En s’inspirant du tutorial sur les animations , on se rend compte qu’il nous faudra utiliser l’objet SingleTickerProviderStateMixin
. Notre widget devra être de type stateful
. Et surtout il nous faudra pas oublier d’implémenter la méthode dispose pour nettoyer les ressources.
Pas très simple tout ça !
1import 'package:flutter/material.dart';
2import 'package:flutter/rendering.dart';
3
4class MyHomePage extends StatefulWidget {
5 MyHomePage({Key key, this.title}) : super(key: key);
6
7 final String title;
8
9 @override
10 _MyHomePageState createState() => _MyHomePageState();
11}
12
13class _MyHomePageState extends State<MyHomePage>
14 with SingleTickerProviderStateMixin {
15 ScrollController _scrollController;
16 AnimationController _hideFabAnimController;
17
18 @override
19 void dispose() {
20 _scrollController.dispose();
21 _hideFabAnimController.dispose();
22 super.dispose();
23 }
24
25 ...
Le code peut effectivement être généré avec les plugins disponibles dans notre IDE ( exemple avec VS Code). Mais cela peut devenir très vite compliqué à maintenir. Surtout si on doit rajouter d’autres controlleurs pour les animations.
Ci-dessous, comment nous pouvons controler l’animation quand l’utilisateur scrolle l’appliation. La lecture du code rajoutant l’animation devrait plutôt être aisée :
27...
28
29 @override
30 void initState() {
31 super.initState();
32 _scrollController = ScrollController();
33 _hideFabAnimController = AnimationController(
34 vsync: this,
35 duration: kThemeAnimationDuration,
36 value: 1, // initially visible
37 );
38
39 _scrollController.addListener(() {
40 switch (_scrollController.position.userScrollDirection) {
41 // Scrolling up - forward the animation (value goes to 1)
42 case ScrollDirection.forward:
43 _hideFabAnimController.forward();
44 break;
45 // Scrolling down - reverse the animation (value goes to 0)
46 case ScrollDirection.reverse:
47 _hideFabAnimController.reverse();
48 break;
49 // Idle - keep FAB visibility unchanged
50 case ScrollDirection.idle:
51 break;
52 }
53 });
54 }
55
56 ...
Après avoir implémenté le controlleur , nous pouvons maintenant connecter le controlleur d’animation avec le widget , en l’occurence ici notre floatingActionButton
53...
54
55 @override
56 Widget build(BuildContext context) {
57 return Scaffold(
58 appBar: AppBar(
59 title: Text(widget.title),
60 ),
61 //body:
62 floatingActionButton: FadeTransition(
63 opacity: _hideFabAnimController,
64 child: ScaleTransition(
65 scale: _hideFabAnimController,
66 child: FloatingActionButton.extended(
67 label: const Text(' Floating Non utilisé'),
68 onPressed: () {},
69 ),
70 ),
71 ),
72 // floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
73 body: ListView(
74 controller: _scrollController,
75 children: <Widget>[
76 for (int i = 0; i < 5; i++) ListItem(index: i + 1),
77 ],
78 ),
79 );
80 }
81 ...
Analysons ce qui ne va pas !
En dehors de notre implémentation de la méthode build
, on pourrait challenger le fait que l’animation soit implémentée dans le widget MyHomePage
. On se rend rapidement compte que nous avons fortement couplé notre logique UI avec ce widget.
L’utilisation des StatefulWidgets
nous obligent à avoir les controlleurs en tant que champs de classe, les initialiser et faire un dispose . On pourrait refactorer en sortant l’appel à scrollController.addListener
pour faire un appel vers une méthode static.
Si nous avions à rajouter de la logique qui n’est pas à liée à notre animation dans cette classe , on se rendrait vite compte que le code deviendrait difficile à maintenir. L’idéal serait d’avoir le maximum de widgets liés à l’affichage de type stateless
.
B. Utilisation des hooks
Nous allons utiliser la librairie Flutter Hooks . A défaut de transformer nos widgets en StatelessWidget
, ils seront transformés en HookWidget
23dependencies:
24 flutter:
25 sdk: flutter
26 flutter_hooks: ^0.14.1
Les Hooks fonctionnement parfaitement avec les State
des StatefulWidget
. Mais il y a une différente majeur à retenir :
Il est impossible d’avoir plusieurs State
associés à un StatefulWidget
, mais on peut avoir plusieurs HookStates
associé à un HookWidget
.
L’un des avantages en utilisant la librairie Flutter Hooks est de pouvoir utiliser des hooks prédéfinis. Par exemple un hook pour obtenir l’ AnimationController
. Nous allons crée notre propre hook pour obtenir et configurer le ScrollController
.
C. De Stateful à HookWidget
Nous allons créer une classe qui ressemble beaucoup à un StatelessWidget
mais ce sera un HookWidget
. Nous n’allons plus utilisé de SingleTickerProviderStateMixin
, de dispose
.
1import 'package:flutter/material.dart';
2import 'package:flutter/rendering.dart';
3import 'package:flutter_hooks/flutter_hooks.dart';
4
5import 'hooks/animation.dart';
6
7class MyHomePage extends HookWidget {
8
9 ...
10}
Création du custom hook pour le scroll !
Notre custom hook doit créer une instance de ScrollController
, y ajouter un listener qui va mettre à jour l’ AnimationController
passé en paramètre, et renvoyer le ScrollController
qui pourra être utilisé dans l’UI de l’application Flutter.
Ci-dessous la création du custom hook :
5ScrollController useScrollControllerForAnimation(
6 AnimationController animationController,
7) {
8 final ScrollController scrollController = ScrollController();
9 scrollController.addListener(() {
10 switch (scrollController.position.userScrollDirection) {
11 // Scrolling up - forward the animation (value goes to 1)
12 case ScrollDirection.forward:
13 animationController.forward();
14 break;
15 // Scrolling down - reverse the animation (value goes to 0)
16 case ScrollDirection.reverse:
17 animationController.reverse();
18 break;
19 case ScrollDirection.idle:
20 break;
21 }
22 });
23 return scrollController;
24}
Nous avons juste transféré toute la logique dans une nouvelle fonction. On resoud ce hook via la méthode build
1class MyHomePage extends HookWidget {
2 MyHomePage({Key key, this.title}) : super(key: key);
3
4 final String title;
5
6 @override
7 Widget build(BuildContext context) {
8 final hideFabAnimController = useAnimationController(
9 duration: kThemeAnimationDuration, initialValue: 1);
10 final scrollController =
11 useScrollControllerForAnimation(hideFabAnimController);
12 return Scaffold(
13 appBar: AppBar(
14 title: Text(title),
15 ),
16 //body:
17 floatingActionButton: FadeTransition(
18 opacity: hideFabAnimController,
19 child: ScaleTransition(
20 scale: hideFabAnimController,
21 child: FloatingActionButton.extended(
22 label: const Text(' Floating Non utilisé'),
23 onPressed: () {},
24 ),
25 ),
26 ),
27 // floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
28 body: ListView(
29 controller: scrollController,
30 children: <Widget>[
31 for (int i = 0; i < 5; i++) ListItem(index: i + 1),
32 ],
33 ),
34 );
35 }
36}
Comprendre la classe utilisée pour le Hook !
Pourquoi une classe et non une fonction pour le hook.
Les hooks fonctionnels sont utilisés si on ne veut pas gérer le cycle de vie du widget. Ici la documentation officielle
Nous utiliserons une classe pour ce hook , car notre logique a besoin de connaitre le cycle de vie du widget qui le porte.
Pour ça , comme présenté ci-dessous, on crée la classe privée _ScrollControllerForAnimationHook
avec son état _ScrollControllerForAnimationHookState
. On constate bien que les Hook
s’implémentent comme les StatefulWidget
13class _ScrollControllerForAnimationHook extends Hook<ScrollController> {
14 final AnimationController animationController;
15
16 const _ScrollControllerForAnimationHook({
17 @required this.animationController,
18 });
19
20 @override
21 _ScrollControllerForAnimationHookState createState() =>
22 _ScrollControllerForAnimationHookState();
23}
24
25class _ScrollControllerForAnimationHookState
26 extends HookState<ScrollController, _ScrollControllerForAnimationHook> {
27 ScrollController _scrollController;
28 @override
29 void initHook() {
30 _scrollController = ScrollController();
31 _scrollController.addListener(() {
32 switch (_scrollController.position.userScrollDirection) {
33 // Scrolling up - forward the animation (value goes to 1)
34 case ScrollDirection.forward:
35 hook.animationController.forward();
36 break;
37 // Scrolling down - reverse the animation (value goes to 0)
38 case ScrollDirection.reverse:
39 hook.animationController.reverse();
40 break;
41 // Idle - keep FAB visibility unchanged
42 case ScrollDirection.idle:
43 break;
44 }
45 });
46 }
47
48 @override
49 ScrollController build(BuildContext context) => _scrollController;
50
51 @override
52 void dispose() => _scrollController.dispose();
53}
pourquoi une classe privée ? Enregistrer le
Hook
avec unHookWidget
se fait en appelant la méthodeuse
. On masque l’appel àuse
en proposant la fonctionuseScrollControllerForAnimation
5ScrollController useScrollControllerForAnimation(
6 AnimationController animationController,
7) {
8 return use(_ScrollControllerForAnimationHook(
9 animationController: animationController,
10 ));
11}
C. Conclusion
Les Hooks
sont très intéressants pour réduire la complexité de notre UI. Avec la librairie
Flutter Hooks , on a la possibilité d’utiliser des hooks prédéfinis ou en créer un custom comme dans ce blog.
La bonne pratique consiste à utiliser les hook prédéfinis et réflechir à 2 fois avant d’en créer un custom.