Séparation des préoccupations dans les applications de flottement
Engineering at ClickUp

Séparation des préoccupations dans les applications de flottement

Récemment, j'ai dû mettre en place des tutoriels d'intégration pour les nouveaux utilisateurs de ClickUp! C'était une tâche très importante, car de nombreux nouveaux utilisateurs étaient sur le point de découvrir la plateforme grâce à la publicité incroyablement drôle que nous avons diffusée lors du Super Bowl! ✨

via ClickUp

Ce guide permet à nos nombreux nouveaux utilisateurs, qui ne connaissent peut-être pas encore ClickUp, de comprendre rapidement comment utiliser plusieurs fonctions de l'application. Il s'agit d'un effort continu, tout comme la nouvelle ressource ClickUp University que nous développons actuellement ! 🚀

Heureusement, l'architecture logicielle derrière l'application mobile ClickUp Flutter m'a permis de mettre en œuvre cette fonction assez rapidement, même en réutilisant les widgets réels de l'application ! Cela signifie que la procédure pas à pas est dynamique, réactive et correspond exactement aux écrans réels de l'application, et qu'elle continuera à l'être, même lorsque les widgets évolueront.

J'ai également pu implémenter cette fonction grâce à une séparation adéquate des préoccupations.

Voyons ce que cela signifie concrètement. 🤔

Séparation des préoccupations

La conception d'une architecture logicielle est l'un des sujets les plus complexes pour les équipes d'ingénieurs. Parmi toutes les responsabilités, il est toujours difficile d'anticiper les évolutions futures des logiciels. C'est pourquoi la création d'une architecture bien stratifiée et découplée peut vous aider, vous et vos coéquipiers, dans de nombreux domaines !

Le principal avantage de la création de petits systèmes découplés est sans aucun doute la testabilité ! Et c'est ce qui m'a aidé à créer une alternative de démonstration des écrans existants de l'application !

Un guide étape par étape

Maintenant, comment pourrions-nous appliquer ces principes à une application Flutter ?

Nous partagerons quelques techniques que nous utilisons pour développer ClickUp à l'aide d'un exemple simple.

L'exemple est si simple qu'il ne permet peut-être pas d'apprécier tous les avantages qu'il recèle, mais croyez-moi, il vous aidera à créer des applications Flutter beaucoup plus faciles à maintenir avec des bases de code complexes. 💡

L'application

À titre d'exemple, nous allons créer une application qui affiche la population des États-Unis pour chaque année.

via ClickUp

Nous avons ici deux écrans :

  • HomeScreen : la liste répertorie toutes les années de 2000 à aujourd'hui. Lorsque les utilisateurs appuient sur une vignette d'année, ils accèdent à l'écran DetailScreen avec un argument de navigation défini sur l'année sélectionnée.
  • DetailScreen : récupère l'année à partir de l'argument de navigation, appelle l'API datausa.io pour cette année et analyse les données JSON afin d'extraire la valeur de population associée. Si les données sont disponibles, un libellé s'affiche avec la population.

Nous nous concentrerons sur l'implémentation de DetailScreen, car elle a le plus grand intérêt avec son appel asynchrone.

Étape 1. Approche naïve

Approche naïve Widget avec état
via ClickUp

La mise en œuvre la plus évidente pour notre application consiste à utiliser un seul StatefulWidget pour l'ensemble de la logique.

Accéder à l'argument de navigation annuelle

Pour accéder à l'année demandée, nous lisons les RouteSettings à partir du widget hérité ModalRoute.

Appel HTTP

Cet exemple invoque la fonction get du package http pour obtenir les données de l'API datausa.io, analyse le résultat du JSON avec la méthode jsonDecode de la bibliothèque dart:convert et conserve Future dans l'état avec une propriété nommée _future.

Rendu

Pour créer l'arborescence des widgets, nous utilisons un FutureBuilder, qui se reconstruit en fonction de l'état actuel de notre appel asynchrone _future.

Révision

D'accord, la mise en œuvre est courte et n'utilise que des widgets intégrés, mais pensez maintenant à notre intention initiale : créer des alternatives de démonstration (ou des tests) pour cet écran. Il est très difficile de contrôler le résultat de l'appel HTTP pour forcer l'application à s'afficher dans un certain état.

C'est là que le concept d'inversion de contrôle nous aidera. 🧐

Étape 2. Inversion du contrôle

Inversion de contrôle vers le fournisseur et le client API
via ClickUp

Ce principe peut être difficile à comprendre pour les nouveaux développeurs (et également difficile à expliquer), mais l'idée générale est d'extraire les préoccupations en dehors de nos composants, afin qu'ils ne soient pas responsables du choix du comportement, et de les déléguer à la place.

Dans une situation plus courante, il s'agit simplement de créer des abstractions et d'injecter des implémentations dans nos composants afin que leur implémentation puisse être modifiée ultérieurement si nécessaire.

Mais ne vous inquiétez pas, tout deviendra plus clair après notre prochain exemple ! 👀

Création d'un objet client API

Afin de contrôler l'appel HTTP vers notre API, nous avons isolé notre implémentation dans une classe DataUsaApiClient dédiée. Nous avons également créé une classe Measure pour faciliter la manipulation et la maintenance des données.

Fournir un client API

Pour notre exemple, nous utilisons le célèbre package fournisseur pour injecter une instance DataUsaApiClient à la racine de notre arborescence.

Utilisation du client API

Le fournisseur permet à tout widget descendant (comme notre DetailScreen) de lire le DataUsaApiClient supérieur le plus proche dans l'arborescence. Nous pouvons ensuite utiliser sa méthode getMeasure pour démarrer notre Future, à la place de l'implémentation HTTP réelle.

Démonstration du client API

Nous pouvons désormais en tirer parti !

Au cas où vous ne le sauriez pas : toutes les classes dans Dart définissent implicitement une interface associée. Cela nous permet de fournir une implémentation alternative de DataUsaApiClient qui renvoie toujours la même instance à partir de ses appels à la méthode getMeasure.

Cette méthode

Cette méthode

Affichage d'une page de démonstration

Nous disposons désormais de toutes les clés pour afficher une instance de démonstration de la DetailPage !

Nous remplaçons simplement l'instance DataUsaApiClient actuellement fournie en encapsulant notre DetailScreen dans un fournisseur, prestataire, qui crée à la place une instance DemoDataUsaApiClient !

Et voilà, notre DetailScreen lit cette instance de démonstration à la place et utilise nos données demoMeasure au lieu d'un appel HTTP.

Révision

C'est un excellent exemple d'inversion de contrôle. Notre widget DetailScreen n'est plus responsable de la logique d'obtention des données, mais la délègue à un objet client dédié. Nous sommes désormais en mesure de créer des instances de démonstration de l'écran ou de mettre en œuvre des tests de widgets pour notre écran ! Génial ! 👏

Mais nous pouvons faire encore mieux !

Comme nous ne sommes pas en mesure de simuler un état de chargement, par exemple, nous ne contrôlons pas entièrement les changements d'état au niveau de nos widgets.

Étape 3. Gestion de l'état

Gestion des instructions dans les applications Flutter
via ClickUp

C'est un sujet brûlant dans Flutter !

Je suis sûr que vous avez déjà lu de longs fils de discussion où des gens tentent de choisir la meilleure solution de gestion d'état pour Flutter. Pour être clair, ce n'est pas ce que nous allons faire dans cet article. À notre avis, tant que vous séparez votre logique métier de votre logique visuelle, tout va bien ! La création de ces couches est vraiment importante pour la maintenabilité. Notre exemple est simple, mais dans les applications réelles, la logique peut rapidement devenir complexe et une telle séparation facilite grandement la recherche de vos algorithmes logiques purs. Ce sujet est souvent résumé sous le terme de gestion d'état.

Dans cet exemple, nous utilisons un ValueNotifier basique avec un fournisseur. Mais nous aurions également pu utiliser flutter_bloc ou riverpod (ou une autre solution), et cela aurait également très bien fonctionné. Les principes restent les mêmes, et tant que vous avez séparé vos états et votre logique, il est même possible de porter votre base de code depuis l'une des autres solutions.

Cette séparation nous aide également à contrôler l'état de nos widgets afin de pouvoir les simuler de toutes les manières possibles !

Création d'un état dédié

Au lieu de nous appuyer sur l'AsyncSnapshot du framework, nous représentons désormais l'état de notre écran sous la forme d'un objet DetailState.

Il est également important d'implémenter les méthodes hashCode et operator == afin de rendre notre objet comparable par valeur. Cela nous permet de détecter si deux instances doivent être considérées comme différentes.

💡 Les paquets equatable ou freezed sont d'excellentes options pour vous aider à implémenter les méthodes hashCode et operator == !

💡 Les paquets equatable ou freezed sont d'excellentes options pour vous aider à implémenter les méthodes hashCode et operator == !

Notre état est toujours associé à une année, mais nous avons également quatre états distincts possibles concernant ce que nous voulons montrer à l'utilisateur :

  • NotLoadedDetailState : la mise à jour des données n'a pas encore commencé.
  • LoadingDetailState : les données sont en cours de chargement.
  • LoadedDetailState : les données ont été chargées avec succès avec une mesure associée.
  • NoDataDetailState : les données ont été chargées, mais aucune donnée n'est disponible.
  • UnknownErrorDetailState : l'opération a échoué en raison d'une erreur inconnue.

Ces états sont plus clairs qu'un AsyncSnapshot, car ils représentent réellement nos cas d'utilisation. Et encore une fois, cela rend notre code plus facile à maintenir !

💡 Nous vous recommandons vivement d'utiliser les types Union du package freezed pour représenter vos états logiques ! Cela ajoute de nombreuses utilités telles que les méthodes copyWith ou map.

💡 Nous vous recommandons vivement d'utiliser les types Union du package freezed pour représenter vos états logiques ! Cela ajoute de nombreuses fonctionnalités utiles, telles que les méthodes copyWith ou mapper.

Intégrer la logique dans un Notifier

Maintenant que nous avons une représentation de notre état, nous devons en stocker une instance quelque part : c'est le rôle de DetailNotifier. Il conservera l'instance DetailState actuelle dans sa propriété valeur et fournira des méthodes pour mettre à jour l'état.

Nous fournissons un état initial NotLoadedDetailState et une méthode de rafraîchissement pour charger les données depuis l'API et mettre à jour la valeur actuelle.

Fournir un état pour la vue

Pour instancier et observer l'état de notre écran, nous nous appuyons également sur le fournisseur et son ChangeNotifierProvider. Ce type de fournisseur recherche automatiquement tout ChangeListener créé et déclenche une reconstruction à partir du consommateur chaque fois qu'il est informé d'un changement (lorsque la valeur de notre notificateur est différente de la précédente).

Révision

Super ! L'architecture de notre application commence à prendre forme. Tout est séparé en couches bien définies, avec des préoccupations spécifiques ! 🤗

Il manque toutefois encore un élément pour la testabilité : nous voulons définir le DetailState actuel afin de contrôler l'état de notre DetailScreen associé.

Étape 4. Widget de disposition visuelle dédiée

Widgets de disposition visuels dédiés dans les applications Flutter
via ClickUp

Dans la dernière étape, nous avons donné un peu trop de responsabilités à notre widget DetailScreen : il était chargé d'instancier le DetailNotifier. Et comme nous l'avons vu précédemment, nous essayons d'éviter toute responsabilité logique au niveau de la couche d'affichage !

Nous pouvons facilement résoudre ce problème en créant une autre couche pour notre widget d'écran : nous allons diviser notre widget DetailScreen en deux :

  • DetailScreen est chargé de configurer les différentes dépendances de notre écran à partir de l'état actuel de l'application (navigation, notificateurs, état, services, etc.).
  • DetailLayout convertit simplement un DetailState en une arborescence dédiée de widgets.

En combinant les deux, nous pourrons créer simplement des instances de démonstration/test DetailLayout, tout en disposant de DetailScreen pour le cas d'utilisation réel dans notre application.

Disposition dédiée

Afin d'obtenir une meilleure séparation des préoccupations, nous avons déplacé tout ce qui se trouvait sous le widget Consumer vers un widget DetailLayout dédié. Ce nouveau widget ne consomme que des données et n'est responsable d'aucune instanciation. Il convertit simplement l'état de lecture en un arbre de widgets spécifique.

Le ModalRoute. de l'appel et l'instance ChangeNotifierProvider restent dans le DetailScreen, et ce widget renvoie simplement le DetailLayout avec une arborescence de dépendances préconfigurée !

Cette amélioration mineure est spécifique à l'utilisation du fournisseur, mais vous remarquerez que nous avons également ajouté un ProxyProvider afin que tout widget descendant puisse directement utiliser un DetailState. Cela facilitera la simulation de données.

Extraction de widgets en tant que classes dédiées

N'hésitez jamais à extraire une arborescence de widgets dans une classe dédiée ! Cela améliorera les performances et rendra le code plus facile à maintenir.

Dans notre exemple, nous avons créé un widget de disposition visuelle pour chacun des types d'état associés :

Exemples d'instances de démonstration

Nous avons désormais un contrôle total sur ce que nous pouvons simuler et afficher sur notre écran !

Il suffit d'envelopper un DetailLayout avec un Provider pour simuler l'état de la disposition.

Conclusion

Créer une architecture logicielle facile à maintenir n'est certainement pas chose aisée ! Anticiper les scénarios futurs peut demander beaucoup d'efforts, mais j'espère que les quelques conseils que j'ai partagés vous aideront à l'avenir !

Les exemples peuvent sembler simples, voire trop sophistiqués, mais à mesure que votre application gagne en complexité, ces normes vous seront d'une grande aide ! 💪

Amusez-vous avec Flutter et suivez le blog pour obtenir d'autres articles techniques comme celui-ci ! Restez à l'écoute !