Tutorial Flutter hooks

post-thumb

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.

Code source ( branche : workshop-five-hooks-0 )


L'implémentation sans les hook est disponible ici.

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 !

home_page.dart
 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 :

home_page.dart
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

home_page.dart
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

pubspec.yaml
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.

home_page.dart
 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 :

hooks/animation.dart
 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

home_page.dart
 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

hooks/animation.dart
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 un HookWidget se fait en appelant la méthode use . On masque l’appel à use en proposant la fonction useScrollControllerForAnimation

hooks/animation.dart
 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.

Code source complet avec le hook ( branche master )


Les sources sont disponibles ici.