Stratégies de mise en cache Flutter pour le cache local, les données obsolètes et les règles de rafraîchissement : que stocker, quand invalider et comment garder les écrans cohérents.

La mise en cache dans une application mobile signifie garder une copie des données à portée (en mémoire ou sur l’appareil) pour qu’un écran suivant puisse s’afficher instantanément au lieu d’attendre le réseau. Ces données peuvent être une liste d’éléments, un profil utilisateur ou des résultats de recherche.
Le problème, c’est que les données en cache sont souvent un peu incorrectes. Les utilisateurs le remarquent vite : un prix qui ne se met pas à jour, un compteur de notifications qui semble bloqué, ou un écran de détails qui affiche des infos anciennes juste après une modification. Ce qui rend le débogage pénible, c’est le timing. Le même endpoint peut sembler correct après un pull-to-refresh, mais faux après une navigation arrière, la reprise de l’app ou un changement de compte.
Il y a un vrai compromis. Si vous récupérez toujours des données fraîches, les écrans paraissent lents et saccadés, et vous gaspillez batterie et data. Si vous cachez agressivement, l’app semble rapide, mais les gens perdent confiance dans ce qu’ils voient.
Un objectif simple aide : rendre la fraîcheur prévisible. Décidez ce que chaque écran est autorisé à afficher (frais, légèrement périmé ou hors ligne), combien de temps les données peuvent vivre avant d’être rafraîchies, et quels événements doivent les invalider.
Imaginez un flux courant : un utilisateur ouvre une commande, puis revient à la liste des commandes. Si la liste vient du cache, elle peut encore afficher l’ancien statut. Si vous rafraîchissez à chaque fois, la liste peut clignoter et donner une impression de lenteur. Des règles claires comme « afficher le cache immédiatement, rafraîchir en arrière-plan et mettre à jour les écrans quand la réponse arrive » rendent l’expérience cohérente lors des navigations.
Un cache n’est pas juste des “données sauvegardées.” C’est une copie sauvegardée plus une règle pour savoir quand cette copie est encore valide. Si vous stockez la charge utile sans la règle, vous vous retrouvez avec deux réalités : un écran montre des infos récentes, un autre montre celles d’hier.
Un modèle pratique consiste à placer chaque élément mis en cache dans un des trois états :
Ce cadrage rend l’UI prévisible, parce qu’elle peut réagir de la même manière chaque fois qu’elle voit un état donné.
Les règles de fraîcheur doivent se baser sur des signaux que vous pouvez expliquer à un coéquipier. Choix courants : expiry temporelle (par exemple 5 minutes), changement de version (schéma ou version de l’app), action utilisateur (pull-to-refresh, soumission, suppression), ou indice serveur (ETag, timestamp last-updated, ou une réponse explicite “cache invalid”).
Exemple : un écran de profil charge instantanément les données utilisateur en cache. Si elles sont périmées-mais-utilisables, il affiche le nom et l’avatar mis en cache, puis rafraîchit discrètement. Si l’utilisateur vient d’éditer son profil, c’est un moment où il faut absolument rafraîchir. L’app doit mettre à jour le cache immédiatement pour que tous les écrans restent cohérents.
Décidez qui possède ces règles. Dans la plupart des apps, le meilleur réglage par défaut est : la couche de données possède la fraîcheur et l’invalidation, l’UI se contente de réagir (afficher le cache, afficher le chargement, afficher l’erreur), et le backend fournit des indices quand il peut. Cela évite que chaque écran invente ses propres règles.
Une bonne mise en cache commence par une question : si ces données sont un peu vieilles, est-ce que cela va nuire à l’utilisateur ? Si la réponse est « probablement non », c’est généralement adapté au cache local.
Les données beaucoup lues et qui changent lentement valent souvent le coup : flux et listes que l’on fait défiler, contenus de type catalogue (produits, articles, templates), et données de référence comme catégories ou pays. Les paramètres et préférences aussi, de même que les informations basiques du profil (nom, URL d’avatar).
Le côté risqué concerne tout ce qui touche à l’argent ou au temps. Soldes, statut de paiement, disponibilité en stock, créneaux de rendez-vous, ETA de livraison et le statut « dernier vu » peuvent poser de vrais problèmes s’ils sont périmés. Vous pouvez les mettre en cache pour la vitesse, mais traitez ce cache comme un placeholder temporaire et forcez un rafraîchissement aux moments de décision (par exemple juste avant la confirmation d’une commande).
L’état UI dérivé est une autre catégorie. Sauver l’onglet sélectionné, des filtres, la requête de recherche, l’ordre de tri ou la position de défilement peut rendre la navigation fluide. Mais cela peut aussi embrouiller quand d’anciennes sélections réapparaissent. Une règle simple marche bien : gardez l’état UI en mémoire tant que l’utilisateur reste dans ce flux, mais réinitialisez-le quand il « recommence » volontairement (par ex. retour à l’écran d’accueil).
Évitez de mettre en cache ce qui crée des risques de sécurité ou de vie privée : secrets (mots de passe, clés API), tokens à usage unique (codes OTP, tokens de reset), et données personnelles sensibles sauf si vous avez vraiment besoin d’un accès hors ligne. Ne cachez jamais les détails complets d’une carte ou tout ce qui augmente le risque de fraude.
Dans une app de shopping, mettre en cache la liste de produits est un gros gain. L’écran de checkout, lui, doit toujours rafraîchir totaux et disponibilités juste avant l’achat.
La plupart des apps Flutter finissent par avoir un cache local pour que les écrans s’affichent vite et n’aient pas d’états vides pendant que le réseau se réveille. La décision clé est l’endroit où résident les données mises en cache, car chaque couche a sa vitesse, ses limites de taille et son comportement de nettoyage.
Un cache en mémoire est le plus rapide. Idéal pour des données que vous venez de récupérer et que vous réutiliserez pendant que l’app est ouverte : profil utilisateur courant, derniers résultats de recherche, ou un produit consulté récemment. L’inconvénient est simple : il disparaît quand l’app est tuée, donc il n’aide pas au cold start ou en hors-ligne.
Le stockage clé-valeur sur disque convient pour de petits éléments que vous voulez conserver entre les redémarrages : préférences, flags, « dernier onglet sélectionné », petits JSON qui changent rarement. Gardez-le volontairement petit. Une fois que vous commencez à y déposer de grandes listes, les mises à jour deviennent compliquées et le bloat arrive vite.
Une base locale est préférable quand vos données sont volumineuses, structurées ou nécessitent un comportement hors-ligne. Elle aide aussi quand vous avez besoin de requêtes (« tous les messages non lus », « articles dans le panier », « commandes du mois dernier ») au lieu de charger un blob et filtrer en mémoire.
Pour garder la mise en cache prévisible, choisissez un magasin principal pour chaque type de données et évitez de garder le même dataset à trois endroits.
Règle simple :
Prévoyez aussi la taille. Décidez ce que « trop gros » veut dire, combien de temps vous gardez les éléments et comment vous nettoyez. Par exemple : limitez les résultats de recherche aux 20 dernières requêtes et supprimez régulièrement les enregistrements vieux de 30 jours pour éviter une croissance silencieuse du cache.
Les règles de rafraîchissement doivent être assez simples pour que vous puissiez les expliquer en une phrase par écran. C’est là que la mise en cache paye : des écrans rapides et une app qui reste fiable.
La règle la plus simple est le TTL (time to live). Sauvegardez les données avec un timestamp et considérez-les fraîches pendant, par exemple, 5 minutes. Après, elles deviennent périmées. Le TTL marche bien pour des données « agréables à avoir » comme un feed, des catégories ou des recommandations.
Un affinement utile est de séparer TTL en soft TTL et hard TTL.
Avec un soft TTL, vous affichez le cache immédiatement, puis rafraîchissez en arrière-plan et mettez à jour l’UI si quelque chose a changé. Avec un hard TTL, vous cessez d’afficher les anciennes données une fois expirées. Vous bloquez avec un loader ou affichez un état « hors ligne / réessayer ». Le hard TTL convient quand se tromper est pire qu’être lent (soldes, statut de commande, permissions).
Si votre backend le supporte, préférez « rafraîchir seulement si changé » via ETag, updatedAt ou un champ version. L’app peut demander « est-ce que ça a changé ? » et éviter de télécharger tout le payload quand rien n’est nouveau.
Un réglage adapté pour beaucoup d’écrans est stale-while-revalidate : afficher tout de suite, rafraîchir discrètement, et redessiner si le résultat diffère. Ça donne de la rapidité sans flicker aléatoire.
Fréquemment, la fraîcheur par écran ressemble à ceci :
Choisissez les règles en fonction du coût d’être dans l’erreur, pas seulement du coût de la requête.
L’invalidation du cache commence par une question : quel événement rend les données mises en cache moins fiables que le coût de les refetcher ? Si vous choisissez un petit ensemble de déclencheurs et vous y tenez, le comportement reste prévisible et l’UI stable.
Les déclencheurs qui comptent le plus dans les apps réelles :
Exemple : un utilisateur modifie sa photo de profil puis revient. Si vous vous fiez uniquement à un rafraîchissement temporel, l’écran précédent peut montrer l’ancienne image jusqu’au prochain fetch. Traitez l’édition comme déclencheur : mettez à jour l’objet profil en cache sur-le-champ et marquez-le comme frais avec un nouveau timestamp.
Garder les règles d’invalidation petites et explicites. Si vous ne pouvez pas pointer l’événement exact qui invalide une entrée de cache, vous rafraîchirez trop souvent (UI lente, saccadée) ou pas assez (écrans périmés).
Commencez par lister vos écrans clés et les données dont chacun a besoin. Ne pensez pas en endpoints. Pensez en objets visibles par l’utilisateur : profil, panier, liste de commandes, fiche produit, compteur de non lus.
Ensuite, choisissez une source de vérité par type de données. En Flutter, c’est souvent un repository qui cache d’où viennent les données (mémoire, disque, réseau). Les écrans ne devraient pas décider quand frapper le réseau. Ils demandent au repository et réagissent à l’état retourné.
Un flux pratique :
Les métadonnées rendent les règles applicables. Si ownerUserId change (logout/login), vous pouvez supprimer ou ignorer immédiatement les anciennes lignes du cache au lieu d’afficher les données du compte précédent pendant un instant.
Pour le comportement UI, décidez à l’avance ce que « périmé » signifie. Une règle commune : afficher les données périmées instantanément pour ne pas laisser l’écran vide, lancer un refresh en arrière-plan, et mettre à jour quand de nouvelles données arrivent. Si le rafraîchissement échoue, gardez les données périmées visibles et affichez une petite erreur claire.
Puis verrouillez les règles avec quelques tests simples :
C’est la différence entre « on a la mise en cache » et « notre app se comporte de la même façon à chaque fois ».
Rien ne casse plus la confiance que de voir une valeur dans une liste, ouvrir le détail, la modifier, puis revenir et voir l’ancienne valeur. La cohérence vient du fait que chaque écran lit la même source.
Règle robuste : fetcher une fois, stocker une fois, rendre plusieurs fois. Les écrans ne doivent pas appeler le même endpoint indépendamment et garder des copies privées. Placez les données en cache dans un store partagé (votre couche de state management), et laissez la liste comme l’écran de détail observer la même donnée.
Gardez un seul endroit qui possède la valeur courante et sa fraîcheur. Les écrans peuvent demander un refresh, mais ils ne devraient pas gérer leurs propres timers, retries et parsing.
Habitudes pratiques pour prévenir les « deux versions de la réalité » :
Même avec de bonnes règles, les utilisateurs verront parfois des données périmées (hors-ligne, réseau lent, app backgroundée). Rendez-le explicite avec des signaux discrets : un timestamp « Mis à jour à l’instant », un indicateur subtil « Rafraîchissement… », ou un badge « Hors ligne ».
Pour les modifications, les updates optimistes fonctionnent souvent mieux. Exemple : un utilisateur change le prix d’un produit sur l’écran de détail. Mettez à jour le store partagé tout de suite pour que la liste affiche le nouveau prix au retour. Si la sauvegarde échoue, annulez et affichez une courte erreur.
La plupart des échecs de cache sont ennuyeux : le cache fonctionne, mais personne ne sait quand l’utiliser, quand il expire et qui en est propriétaire.
Le premier piège est cacher sans métadonnées. Si vous ne stockez que le payload, vous ne pouvez pas dire s’il est vieux, quelle version de l’app l’a produit ou à quel utilisateur il appartient. Sauvegardez au moins savedAt, un simple numéro de version et un userId. Cette habitude évite beaucoup de bugs « pourquoi cet écran est faux ? »
Un autre problème courant est d’avoir plusieurs caches pour les mêmes données sans propriétaire clair. Une liste garde une liste en mémoire, un repository écrit sur disque, et un écran de détail refetch et écrit ailleurs. Choisissez une source de vérité (souvent la couche repository) et faites en sorte que chaque écran lise par son intermédiaire.
Les changements de compte sont un piège fréquent. Si quelqu’un se déconnecte ou change de compte, videz les tables et clés user-scopées. Sinon vous risquez d’afficher la photo ou les commandes de l’utilisateur précédent pendant un instant, ce qui ressemble à une fuite de vie privée.
Corrections pratiques :
Exemple : votre liste de produits charge instantanément depuis le cache, puis rafraîchit discrètement. Si le rafraîchissement échoue, continuez d’afficher le cache mais signalez qu’il peut être obsolète et proposez Réessayer. Ne bloquez pas l’UI sur un refresh quand le cache ferait l’affaire.
Avant la release, transformez la mise en cache de « ça a l’air correct » en règles testables. Les utilisateurs doivent voir des données cohérentes même après navigation, hors-ligne ou changement de compte.
Pour chaque écran, décidez combien de temps les données peuvent être considérées fraîches. Ce peut être des minutes pour des données rapides (messages, soldes) ou des heures pour des données lentes (paramètres, catégories produits). Confirmez ensuite ce qui se passe quand ce n’est plus frais : rafraîchissement en arrière-plan, rafraîchissement à l’ouverture, ou pull-to-refresh manuel.
Pour chaque type de données, décidez quels événements doivent vider ou bypasser le cache. Déclencheurs courants : logout, édition de l’élément, changement de compte, et mises à jour de l’app qui changent la forme des données.
Assurez-vous que les entrées de cache stockent un petit ensemble de métadonnées à côté du payload :
Gardez la propriété claire : un repository par type de données (ex. ProductsRepository), pas par widget. Les widgets demandent des données, ils ne décident pas des règles de cache.
Décidez et testez aussi le comportement hors-ligne. Confirmez quels écrans affichent le cache, quelles actions sont désactivées, et quel texte afficher (« Affiche les données sauvegardées » plus un contrôle de rafraîchissement visible). Le rafraîchissement manuel doit exister sur chaque écran supporté par le cache et être facile à trouver.
Imaginez une simple app de boutique avec trois écrans : catalogue (liste), détail produit et onglet Favoris. Les utilisateurs défilent le catalogue, ouvrent un produit et tapent sur une icône cœur pour le mettre en favoris. L’objectif : réactivité même sur réseaux lents, sans incohérences confuses.
Cachez localement ce qui aide à rendre l’écran instantanément : pages de catalogue (IDs, titre, prix, URL de vignette, flag favori), détails produit (description, specs, disponibilité, lastUpdated), métadonnées d’images (URLs, tailles, clés de cache) et la liste de favoris de l’utilisateur (ensemble d’IDs, éventuellement avec timestamps).
Quand l’utilisateur ouvre le catalogue, affichez les résultats mis en cache immédiatement, puis revalidez en arrière-plan. Si des données fraîches arrivent, mettez à jour seulement ce qui a changé en gardant la position de défilement.
Pour le toggle favori, traitez-le comme une action « doit être cohérente ». Mettez à jour l’ensemble local des favoris tout de suite (optimistic update), puis mettez à jour les lignes produits en cache et les détails du produit pour cet ID. Si l’appel réseau échoue, revenez en arrière et affichez un court message.
Pour maintenir la cohérence en navigation, alimentez à la fois les badges de la liste et l’icône cœur du détail depuis la même source de vérité (votre cache local ou store), pas depuis des états d’écran séparés. Le cœur de la liste se met à jour dès le retour depuis le détail, le détail reflète les changements faits depuis la liste, et le compte de l’onglet Favoris correspond partout sans attendre un refetch.
Ajoutez des règles de rafraîchissement simples : le cache du catalogue expire rapidement (minutes), les détails produit un peu plus tard, et les favoris n’expirent pas mais se réconcilient toujours après login/logout.
La mise en cache cesse d’être mystérieuse quand votre équipe peut pointer une seule page de règles et s’accorder sur ce qui doit se passer. L’objectif n’est pas la perfection mais un comportement prévisible qui dure entre les releases.
Rédigez une petite table par écran et gardez-la assez courte pour être relue quand vous changez quelque chose : nom de l’écran et données principales, emplacement et clé du cache, règle de fraîcheur (TTL, événementiel ou manuel), déclencheurs d’invalidation, et ce que voit l’utilisateur pendant le rafraîchissement.
Ajoutez un logging léger pendant le tuning. Enregistrez hits/misses de cache et pourquoi un refresh a eu lieu (TTL expiré, pull-to-refresh, reprise de l’app, mutation complétée). Quand quelqu’un rapporte « cette liste est bizarre », ces logs rendent le bug traçable.
Commencez avec des TTL simples, puis affinez selon ce que remarquent vos utilisateurs. Un fil d’actualité peut tolérer 5 à 10 minutes de péremption, tandis qu’un écran de statut de commande peut nécessiter un rafraîchissement à la reprise et après chaque action de checkout.
Si vous construisez vite en Flutter, il aide de décrire votre couche de données et vos règles de cache avant d’implémenter quoi que ce soit. Pour les équipes utilisant Koder.ai (koder.ai), le mode planning est un bon endroit pour écrire d’abord ces règles par écran, puis construire pour s’y conformer.
Quand vous ajustez le comportement de rafraîchissement, protégez les écrans stables pendant l’expérimentation. Des snapshots et rollback font gagner du temps quand une nouvelle règle introduit involontairement du flicker, des états vides ou des comptes incohérents en navigation.
Commencez par une règle claire par écran : ce qu’il peut afficher immédiatement (mis en cache), quand il doit se rafraîchir, et ce que voit l’utilisateur pendant le rafraîchissement. Si vous ne pouvez pas expliquer la règle en une phrase, l’application finira par être incohérente.
Considérez les données mises en cache comme ayant un état de fraîcheur. Si elles sont fraîches, affichez-les. Si elles sont périmées mais utilisables, affichez-les et rafraîchissez silencieusement. Si elles sont en obligation de rafraîchir, récupérez-les avant d’afficher (ou montrez un état de chargement/hors ligne). Cela rend le comportement de l’UI cohérent au lieu de « parfois ça se met à jour, parfois non ».
Mettez en cache ce qui est lu souvent et peut être un peu vieux sans nuire à l’utilisateur : flux, catalogues, données de référence et infos de profil basiques. Méfiez-vous des données sensibles au temps ou à l’argent (soldes, disponibilité en stock, ETA, statut de commande) : vous pouvez les mettre en cache pour la vitesse, mais forcez un rafraîchissement au moment de la décision ou de la confirmation.
Utilisez la mémoire pour une réutilisation rapide pendant la session courante (profil courant, éléments récemment consultés). Utilisez un stockage clé-valeur sur disque pour de petits éléments simples qui doivent survivre aux redémarrages (préférences). Utilisez une base locale quand les données sont volumineuses, structurées, nécessitent des requêtes ou doivent fonctionner hors ligne (messages, commandes, inventaires).
Un TTL simple est un bon défaut. Considérez les données comme fraîches pendant une durée donnée, puis rafraîchissez-les. Pour une meilleure expérience, préférez « afficher le cache tout de suite, revalider en arrière-plan et mettre à jour si nécessaire » (stale-while-revalidate), car cela évite les écrans vides et réduit le flicker.
Invalidez sur les événements qui changent clairement la confiance dans le cache : éditions utilisateur (create/update/delete), login/logout ou changement de compte, reprise de l’app si les données dépassent le TTL, et rafraîchissement explicite de l’utilisateur. Gardez ces déclencheurs réduits et explicites pour éviter de rafraîchir trop souvent ou pas assez.
Faites en sorte que les deux écrans lisent la même source de vérité, pas des copies privées. Quand l’utilisateur modifie quelque chose sur l’écran de détail, mettez à jour l’objet partagé en local immédiatement afin que la liste affiche la nouvelle valeur au retour, puis synchronisez avec le serveur et annulez seulement en cas d’échec de sauvegarde.
Stockez des métadonnées avec le payload, surtout un timestamp et un identifiant utilisateur. À la déconnexion ou au changement de compte, effacez ou isolez immédiatement les entrées de cache liées à l’utilisateur et annulez les requêtes en cours liées à l’ancien compte pour éviter d’afficher brièvement les données du précédent utilisateur.
Gardez les données périmées visibles par défaut et affichez un petit état d’erreur clair avec option de réessayer, plutôt que de vider l’écran. Si l’écran ne peut pas afficher des données anciennes en toute sécurité, passez à une règle « must-refresh » et affichez un message de chargement ou hors ligne au lieu de présenter une valeur obsolète comme fiable.
Placez la logique de cache dans votre couche de données (par exemple, des repositories) afin que chaque écran suive le même comportement. Les widgets doivent simplement réagir aux états renvoyés (cache, chargement, erreur) et ne pas implémenter leurs propres règles de rafraîchissement. Si vous utilisez Koder.ai, décrivez d’abord les règles par écran en planning mode, puis implémentez-les pour que l’UI reste passive.