Un package est en cours de création pour mettre en place une stratégie de cache rapidement et facilement : https://github.com/romgrm/flutter_cache_strategy. La stratégie de cache en flutter est un aspect primordial dans le développement d’une application mobile.
Dans cet article, je vais vous expliquer comment, chez Beapp, on l’implémente.
Si vous lisez cet article, je pense que vous savez ce qu’est le cache, mais au cas-où…
Le cache correspond au stockage de données dans la mémoire de votre appareil, quel qu’il soit. L’idée derrière est de manipuler cette donnée.
Mais comme une image vaut mille mots :
Comme vous pouvez le voir, le principal objectif d’une stratégie de cache est d’essayer de toujours afficher quelque chose à l’écran.
Concernant les données sensibles évoquées plus haut, je dissocie bien le cache utilisateur du cache réseau pour ces raisons :
C’est donc une bonne pratique de séparer ces caches, même si concrètement, ils peuvent être combinés.
Maintenant que nous avons une vision primaire de ce qu’est le cache, rentrons dans le vif du sujet !
Au début du projet, l’arborescence de dossiers ressemble à ça :
Dans le sous dossier storage
on crée un fichier storage.dart
qui contient une classe abstraite Storage
.
Cette classe est un contrat dans lequel on déclare des méthodes pour manipuler la data.
Comme je le disais, on va manipuler de la donnée à travers l’application, mais pour ça, il faut au préalable la stocker dans l’appareil.
On va donc utiliser le package Hive, qui est une solution de stockage sur le modèle clef:valeur
.
Pour expliquer brièvement, Hive va créer un dossier dans le stockage interne de l’appareil dans lequel une hiveBox contiendra la donnée.
On peut par la suite facilement ouvrir cette box et accéder à la donnée souhaitée pour la manipuler.
Pour implémenter ces méthodes, on crée un fichier cache_storage.dart
, toujours dans le sous-dossier Storage
.
Le principe est simple :
Hive
à l’instanciation de CacheStorage
.hiveBoxName
et la méthode appelée est exécutée (get, write, delete…).key
.Maintenant que l’on peut manipuler la donnée avec ces méthodes, on va mettre en place différentes stratégies qui correspondront aux différents cas d’utilisation de notre application.
On commence par créer un fichier cache_strategy.dart
à la racine de notre dossier cache
. Ce contrat nous permet d’uniformiser l’application des différentes stratégies existantes.
CacheWrapper
.defaultTTLValue
.asyncBloc
. Il appellera ensuite _storeCacheData()
pour stocker cette dernière, puis la renvoyer.key
. Une fois le json reçu et décodé par leCacheWrapper
, si la donnée est toujours valide, on retourne un objet Dart typé et utilisable grâce au serializerBloc
.
Avec ces explications, on peut voir le déroulement de chaque stratégie implémentée :
➜ On appelle applyStrategy()
pour indiquer quelle stratégie est utilisée avec les paramètres requis.
➜ Pour récupérer la donnée en cache, fetchCacheData()
est utilisée, qui elle-même utilise _isValid()
pour vérifier si la donnée est toujours valide, auquel cas la renverra.
➜ Pour récupérer de la donnée via un appel réseau, on utilise invokeAsync()
qui, une fois la donnée récupérée, la stockera en cache via _storeCacheData()
puis la renverra également.
Concernant le CacheWrapper
évoqué plus haut, on crée un fichier cache_wrapper.dart
à la racine de cache
.
CacheWrapper
est une classe qui, comme son nom l’indique, permet de contenir la donnée reçue. Son constructeur prend deux arguments, un type générique data
qui contiendra la donnée de n’importe quel type et cachedDate
qui contiendra l’heure exacte à laquelle la donnée a été stockée.
Les méthodes fromJson()
et toJson()
permettent de convertir la donnée, soit en JSON pour la mise en cache, soit en Map
pour l’utiliser dans le code.
CacheWrapper
peut donc être interprété comme un “wrapper” qui contient la donnée stockée et qui permet d’encoder/décoder cette dernière.
À cette étape de l’article, notre arborescence de fichiers ressemble à ça :
Maintenant que nous avons vu la définition de ce que peuvent faire nos différentes stratégies, passons à leur implémentation.
Dans un nouveau dossier strategy
à la racine de cache
, on va créer autant de fichier qu’il y a de stratégies.
Chaque stratégie est un singleton, ce qui signifie qu’une seule instance de ces dernières sera injectée dans l’application.
On aurait pu utiliser le package get_it pour injecter nos stratégies, mais cela accroît la dépendance à une librairie tierce et ses désavantages que l’on connait, donc il est préférable de les créer soi-même.
Chaque stratégie va implémenter la méthode applyStrategy()
de la classe abstraite CacheStrategy
.
AsyncOrCache
Cette stratégie va déclencher un appel réseau dans le but de récupérer de la donnée. Si de la donnée est récupérée, elle sera stockée et renvoyée repository. Dans le cas o une erreur est retournée suite à notre appel réseau (erreur 401, 403, 404, 500…), la stratégie va récupérer de la data stockée précédemment en cache puis la retourner. S’il n’y a pas de donnée en cache ou qu’elle est invalide, on retourne l’erreur précédemment levée ainsi qu’une valeur null
qui sera gérée dans le gestionnaire d’état.
CacheOrAsync
Cette stratégie est similaire à celle du dessus, mais dans le sens inverse. On va d’abord checker si de la donnée est stockée en cache et, si le retour de fetchCacheData()
est null
, on déclenchera un appel réseau. La suite est identique au comportement de la stratégie AsyncOrCache
.
JustAsync
Cette stratégie déclenche seulement un appel réseau, avec le même comportement qu’AsyncOrCache
.
JustCache
Cette dernière utilise donc uniquement la donnée stockée en cache.
Pour les deux dernières stratégies, un appel réseau/cache aurait suffi pour avoir le résultat souhaité, mais l’uniformisation est une bonne pratique dans le monde du développement.
À présent, on doit créer un point d’entrée pour que nos stratégies soient utilisées.
À la racine de cache
, on crée un fichier cache_manager.dart
.
Ce fichier contient toute la logique et les entrants pour implémenter la stratégie adéquate. Il sera directement utilisé dans notre code métier.
➜ Ce fichier est séparé en deux classes : CacheManager
et StrategyBuilder
.
➜ CacheManager
contient le point d’entrée avec la méthode from()
. StrategyBuilder
contient les autres méthodes qui permettent l’implémentation de la stratégie.
firstName + lastName + id
de l’utilisateur comme defaultSessionName
.StrategyBuilder
qui prend un type générique (ce qui permet donc de typer la data comme on le souhaite). Une key
est passée en paramètre afin de créer le nom de notre future hiveBox
. Une instance de CacheStorage
est également passée afin que le StrategyBuilder
puisse s’en servir. withSession() permet donc de nommer la session de cache actuelle.prefix
qui correspond à la key
de la donnée stockée.Une fois que la méthode from()
est appelée, le StrategyBuilder
déclenche les méthodes suivantes :
invokeAsync()
.La sérialisation / désérialisation par défaut dans Dart n’étant pas optimisée pour les objets complexes, Flutter conseille vivement d’utiliser le package json_serializable. Il va automatiquement générer les méthodes toJson()
et fromJson()
pour chacun de nos DTOs, qui seront appelées lorsque le serializerBloc
sera utilisé dans la stratégie de cache.
defaultTTLValue
pour la durée de vie du cache.enum
par exemple).applyStrategy()
et par la suite de retourner la donnée récupérée.Maintenant que nous avons vu comment s’implémente la stratégie de cache, passons du côté métier de notre application pour comprendre comment s’en servir.
Tout d’abord, nous devons injecter le CacheManager
qui est le point d’entrée de notre stratégie.
Je vous conseille de créer un fichier service_locator.dart
à la racine de core
, qui gère l’injection de dépendances dans notre app.
On a donc un singleton de CacheManager
avec l’instance pour le stockage CacheStorage
.
La méthode setupGetIt()
est généralement appelée à l’initialisation de l’application, dans le main.dart
.
En essayant d’appliquer au mieux le principe de Separation of Concerns et le pattern Clean Architecture, notre arborescence ressemble à ça :
Ce qui nous intéresse ici, c’est le repository
, qui fait office de “pont” en recevant un dto
qui provient du datasource
, pour le transformer en entity
, provenant du domain
.
C’est donc à ce moment-là où la donnée reçue en brute sera transformée en objet adapté à notre application, et donc logiquement ici ou la mise en cache s’applique.
Prenons un exemple d’une application qui affiche un travail à faire pour un étudiant.
On va d’abord récupérer les instances de HomeworkDataSource
et CacheManager
afin de pouvoir récupérer respectivement l’appel réseau ainsi que la stratégie de cache.
getHomeworkAssignment
permet de récupérer une liste de HomeworkDto
et de la convertir en liste de HomeworkEntity
.
Dans cette conversion, on implémente notre mise en cache comme telle :
dto
attendu ainsi que la key
de la session de cache.Avec cette implémentation, notre stratégie va d’abord appeler fetchHomeworkAssignment()
pour récupérer de la donnée depuis le serveur. Si une erreur est renvoyée, alors un appel sur le cache est déclenché pour récupérer de la donnée mise en cache sur l’appareil. À la fin, soit de la donnée (récente ou non), soit null
sera retourné. Il ne reste plus qu’à appeler le repository
, par exemple avec un gestionnaire d’état, pour pouvoir ensuite gérer l’affichage dans notre UI.
J’espère avoir été assez clair dans ces explications, si l’implémentation de cette stratégie vous paraît trop longue et que vous voulez un résultat rapidement, un package est bientôt disponible !
Merci pour la lecture, tout commentaire/retour est le bienvenu ! 🙏
Romain / Développeur mobile chez Beapp
—
Les maladies inflammatoires chroniques de l’intestin ou "MICI" sont invisibles, mais leurs impacts sur la…
Depuis l'été, j'ai un Pixel qui intègre à la fois un TPU (Tensor Processing Unit)…
On se retrouve dans un nouvel article avec toutes les infos sur cette nouvelle saison…
Pourquoi l’inclusion numérique est essentielle : le point avec Mathieu Froidure. Dans un monde de…
Elles sont passées où les femmes dans la tech ? Entre le manque de représentation…