Les viewModels persistent en mémoire?

Bonjour à tous et merci à @mbritto pour la qualité du contenu que tu proposes.

Contexte
J’ai suivi le cours sur l’architecture et la navigation pour Flutter et j’ai voulu mettre en pratique les concepts abordés dans mon app.
Rapidement, j’ai compris que j’allais devoir mettre en cache mes données récupérées du backend pour éviter les appels inutiles à l’API.
Pour cela j’ai créé une classe Repository qui hérite de ChangeNotifier et qui contient les données en cache, dans le but que mes viewModels puissent écouter les changements sur le cache et être notifiés lorsqu’une donnée fraîche arrive (pour ensuite mettre à jour les vues correspondantes).

Problème
Pour faire cela, j’appelle la méthode monRepository.addListener(maFonctionPourUpdateLaVue) dans le constructeur de mon viewModel, et pour libérer les ressources une fois que la page est détruite j’ai overridé la fonction dispose() de mon viewModel dans laquelle j’ai mis monRepository.removeListener(maFonctionPourUpdateLaVue).
Le souci c’est que je me suis aperçu que la fonction dispose() n’est en fait jamais appelée, car visiblement ce n’est le cas que pour les classes héritant de StatefullWidgets. Cela semble vouloir dire que lorsqu’on fait _monViewModel = null; dans le RouterDelegate pour supprimer la vue (lorsqu’on revient en arrière par exemple), les ressources du viewModel ne sont pas libérées, est-ce que c’est correct ?

Solution envisagée
Pour résoudre ce problème j’envisage d’appeler manuellement la fonction .dispose() de chaque viewModel dans mon RouterDelegate avant chaque _monViewModel = null;.
Cela me paraît un peu rébarbatif et si j’en oublie j’ai un risque de fuite mémoire. Connaissez-vous une meilleure solution ?

Merci d’avance pour votre aide :slight_smile:

2 « J'aime »

Hello Paul,
Je pense avoir rencontré un problème similaire au tiens. Si j’ai bien compris ta demande du peux essayer le widget PopScope dans le code de ta vue.

Il permet de faire appel à une fonction lorsque tu fermes ta vue :

PopScope(
            onPopInvokedWithResult: (didPop, result) async {
              await widget.viewmodel.TAFONCTIONDISPOSE;
            },
child : LECODETAVUE }

Je pense que tu dois pouvoir mettre ainsi ta fonction removeListener dans ton viewmodel.

Salut Xavier, merci pour ta réponse et pour ta réactivité !
Je ne connaissais pas PopScope, effectivement ça m’a l’air déjà plus clean dans la manière de procéder.
Si je comprends bien, comme une grande majorité de mes viewModels écoutent les changements du cache, il faudrait que j’englobe le contenu de chaque page dans un PopScope dans ce cas ?
Peut-être que cela cache un problème d’archi plus profond dans mon app. J’ai le sentiment qu’il doit y avoir une solution plus efficace que ce que j’ai mis en place pour gérer la mise à jour des vues suite à un changement de cache, j’ai du mal à trouver la solution idéale sans utiliser de dépendance externe.

Oui c’est ce que j’ai fais a pas mal d’endroits. Je ne sais pas si c’est le mieux, mais avant de trouver ce truc je faisais des choses vraiment tordues dans le viewmodel, la view, et le navigationdelegate.
Ce qui commence a me poser problème c’est l’imbrication avec les FutureBuilder et AnimatedBuilder qui commencent à faire un peu lourd… Je crois que Maxime avait fait une bibliothèque pour les deux derniers points.

Discussion super intéressante que je découvre à peine :slight_smile:

A propos de dispose()

Cette fonction n’est pas une fonction de dart, mais une fonction de Flutter, et plus précisément de la classe State. Elle ne sera donc disponible que pour les widget de type StatefulWidget. Dans un view model, qui est une classe Dart classique, il n’y a pas de notion de destructeur (fonction appelée automatiquement à la destruction d’un objet pour faire le ménage).

Quand dispose() est-elle appelée ?

Au moment où le widget est retiré de l’écran définitivement. C’est différent du moment où il est masqué par un autre widget.

Exemple :

si j’ai une écran de type liste et un écran de type détails où je navigue après avoir cliqué sur un élément de ma liste.

  • Si je suis sur l’écran de détail d’un élément et que je clique sur retour, alors l’écran de détail sera retiré définitivement et je reviendrai sur l’écran de liste. dispose() sera appelé sur l’objet State de l’écran de détails
  • Si je suis sur l’écran de liste et que navigue vers la vue de détail d’un élément, l’écran de liste sera simplement recouvert par celui de détail, et non retiré. dispose() ne sera pas appelé sur l’écran de liste.

Solutions possibles ?

Voici 2 solutions que tu peux mettre en place en fonction de tes besoins :

  • Utiliser des StatefulWidget et la fonction dispose() pour appeler une fonction de nettoyage dans ton view model au moment où le State te prévient. Mais ça ne marchera que lors de la suppression des écrans et non lorsqu’ils seront recouverts. Note qu’il vaut mieux utiliser cette fonction en parallèle avec initState() plutôt qu’avec le constructeur de ton viewmodel.
  • Mettre en place ton propre système depuis ton routeur en prévenant tes view models à chaque fois qu’ils vont être affichés ou masqués. Comme ça tu as 100% du contrôle pour déclencher et arrêter tes chargements. Nous en avons parlé sur les dernières séances de coachings de groupe et j’ai même montré quelques exemples de projets à moi avec cette mécanique.

Et PopScope() ?

Je ne le connais pas et il fait probablement très bien le job. Mais vous connaissez probablement ma tendance à limiter au maximum les dépendances dans mes projets :grin: C’est pour cette raison que je préfère conserver le contrôle au niveau de mon routeur et alléger ainsi mes widgets.

3 « J'aime »

Merci pour cette réponse très complète ! Je vais regarder du côté de la deuxième solution que tu suggères qui me paraît mieux correspondre à mes besoins.

J’avais peur de ne pas gérer correctement le fonctionnement du cache avec cette architecture mais la mécanique dont tu parles me rassure et me conforte dans mes choix :slightly_smiling_face: