Réseau Kotlin pour connexions lentes : timeouts et retries sûrs
Kotlin réseau pour connexions lentes : définissez des timeouts adaptés, mettez en cache en sécurité, réessayez sans doublons et protégez les actions critiques sur réseaux mobiles instables.

Ce qui casse sur des connexions lentes et instables
Sur mobile, « lent » ne signifie généralement pas « pas d’internet ». Cela veut souvent dire une connexion qui fonctionne par courtes rafales. Une requête peut prendre 8 à 20 secondes, se bloquer à mi-chemin, puis se terminer. Ou elle peut réussir un instant et échouer le suivant parce que le téléphone est passé du Wi‑Fi à la 4G, est entré dans une zone de faible signal, ou que l’OS a mis l’app en arrière‑plan.
« Instable » est pire. Des paquets sont perdus, des résolutions DNS expirent, des handshakes TLS échouent et des connexions se réinitialisent au hasard. Vous pouvez tout faire « bien » dans le code et voir des échecs en production parce que le réseau change sous vos pieds.
C’est là que les réglages par défaut ont tendance à casser. Beaucoup d’apps s’appuient sur les valeurs par défaut des bibliothèques pour les timeouts, retries et caches sans définir ce que « suffisamment bon » signifie pour de vraies personnes. Les valeurs par défaut sont souvent ajustées pour un Wi‑Fi stable et des APIs rapides, pas pour un train de banlieue, un ascenseur ou un café bondé.
Les utilisateurs ne décrivent pas des « socket timeouts » ou des « HTTP 503 ». Ils remarquent des symptômes : des spinners infinis, des erreurs subites après une longue attente (puis ça marche au prochain essai), des actions dupliquées (deux réservations, deux commandes, doubles prélèvements), des mises à jour perdues et des états mixtes où l’UI affiche « échec » alors que le serveur a en réalité réussi.
Les réseaux lents transforment de petits manques de conception en problèmes d’argent et de confiance. Si l’app ne sépare pas clairement « en cours d’envoi », « échoué » et « terminé », les utilisateurs appuient à nouveau. Si le client réessaye sans réfléchir, il peut créer des doublons. Si le serveur ne supporte pas l’idempotence, une connexion instable peut produire plusieurs écritures « réussies ».
Les « actions critiques » sont tout ce qui doit arriver au plus une fois et doit être correct : paiements, commandes, réservation d’un créneau, transfert de points, changement de mot de passe, sauvegarde d’une adresse de livraison, dépôt d’une réclamation ou envoi d’une approbation.
Un exemple réaliste : quelqu’un soumet un paiement sur une 4G faible. L’app envoie la requête, puis la connexion tombe avant l’arrivée de la réponse. L’utilisateur voit une erreur, appuie sur « Payer » à nouveau, et maintenant deux requêtes arrivent au serveur. Sans règles claires, l’app ne sait pas si elle doit réessayer, attendre ou arrêter. L’utilisateur ne sait pas s’il doit tenter à nouveau.
Décidez de vos règles avant d’ajuster le code
Quand les connexions sont lentes ou instables, la plupart des bugs viennent de règles floues, pas du client HTTP. Avant de toucher aux timeouts, cache ou retries, notez ce que « correct » signifie pour votre app.
Commencez par les actions qui ne doivent jamais s’exécuter deux fois. Ce sont généralement des actions liées à l’argent et au compte : passer une commande, débiter une carte, envoyer un paiement, changer un mot de passe, supprimer un compte. Si un utilisateur appuie deux fois ou si l’app réessaie, le serveur doit quand même traiter cela comme une seule requête. Si vous ne pouvez pas le garantir, considérez ces endpoints comme « pas de retry automatique » tant que ce n’est pas réglé.
Ensuite, décidez ce que chaque écran est autorisé à faire quand le réseau est mauvais. Certains écrans peuvent rester utiles hors‑ligne (profil connu, commandes précédentes). D’autres devraient passer en lecture seule ou afficher un état « réessayer » clair (stocks, prix en temps réel). Mélanger ces attentes mène à une UI confuse et à un caching risqué.
Fixez un temps d’attente acceptable par action en vous basant sur la perception utilisateur, pas sur ce qui est propre dans le code. La connexion peut tolérer une courte attente. Un upload de fichier aura besoin de plus. Le checkout doit paraître rapide mais aussi sûr. Un timeout de 30 secondes peut être « fiable » sur le papier et pourtant sembler cassé.
Enfin, décidez ce que vous stockez sur l’appareil et pour combien de temps. Le cache aide, mais des données périmées peuvent provoquer de mauvais choix (anciens prix, éligibilité expirée).
Écrivez ces règles quelque part où tout le monde peut les trouver (un README suffit). Gardez‑les simples :
- Quels endpoints ne doivent pas être dupliqués et requièrent une gestion d’idempotence ?
- Quels écrans doivent fonctionner hors‑ligne, et lesquels sont lecture seule hors‑ligne ?
- Quel est le temps d’attente maximum par action (login, rafraîchissement du feed, upload, checkout) ?
- Que peut‑on mettre en cache sur l’appareil et quelle est la durée d’expiration ?
- Après un échec, affichez‑vous une erreur, mettez‑vous en file d’attente pour plus tard, ou exigez‑vous une reprise manuelle ?
Quand ces règles sont claires, vos valeurs de timeout, en‑têtes de cache, politique de retry et états UI sont beaucoup plus simples à implémenter et tester.
Timeouts qui correspondent aux attentes réelles
Les réseaux lents échouent de différentes façons. Une bonne configuration de timeouts ne consiste pas juste à « choisir un nombre ». Elle correspond à ce que l’utilisateur essaie de faire et échoue assez vite pour que l’app puisse récupérer.
Les trois timeouts, en termes simples :
- Connect timeout : combien de temps attendre pour établir une connexion au serveur (résolution DNS, TCP, TLS). Si cela échoue, la requête n’a jamais vraiment démarré.
- Write timeout : combien de temps attendre pendant l’envoi du corps de la requête (uploads, JSON volumineux, uplink lent).
- Read timeout : combien de temps attendre que le serveur renvoie des données après l’envoi. C’est souvent ce qui se manifeste sur des réseaux mobiles instables.
Les timeouts doivent refléter l’écran et les enjeux. Un fil d’actualité peut être plus lent sans réel dommage. Une action critique doit soit se terminer, soit échouer clairement pour que l’utilisateur sache quoi faire.
Un point de départ pratique (ajustez après mesure) :
- Chargement de liste (faible risque) : connect 5–10 s, read 20–30 s, write 10–15 s.
- Recherche au fur et à mesure : connect 3–5 s, read 5–10 s, write 5–10 s.
- Actions critiques (haut risque, comme “Payer” ou “Soumettre commande”) : connect 5–10 s, read 30–60 s, write 15–30 s.
La cohérence compte plus que la perfection. Si l’utilisateur appuie sur « Soumettre » et voit un spinner pendant deux minutes, il va retaper.
Évitez les “chargements infinis” en ajoutant aussi une limite haute claire dans l’UI. Affichez un progrès immédiatement, permettez l’annulation, et après (disons) 20–30 secondes affichez « Toujours en cours… » avec des options pour réessayer ou vérifier la connexion. Cela garde l’expérience honnête même si la librairie réseau est toujours en attente.
Quand un timeout survient, loggez assez pour déboguer des motifs plus tard, sans enregistrer de secrets. Champs utiles : chemin d’URL (pas la query complète), méthode HTTP, statut (si disponible), répartition des timings (connect vs write vs read si disponible), type de réseau (Wi‑Fi, cellulaire, mode avion), taille approximative requête/réponse, et un request ID pour faire correspondre logs client et serveur.
Une configuration Kotlin simple et cohérente
Quand les connexions sont lentes, de petites incohérences dans la configuration client deviennent de gros problèmes. Une base propre vous aide à déboguer plus vite et donne à chaque requête les mêmes règles.
Un client, une politique
Commencez par un seul endroit où vous construisez votre client HTTP (souvent un OkHttpClient utilisé par Retrofit). Mettez‑y les basiques pour que chaque requête se comporte de la même façon : en‑têtes par défaut (version de l’app, locale, token), un User‑Agent clair, timeouts définis une fois (pas éparpillés), logging activable pour le débogage, et une décision unique sur les retries (même si c’est « pas de retry automatique »).
Voici un petit exemple qui garde la configuration dans un seul fichier :
val okHttp = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.callTimeout(30, TimeUnit.SECONDS)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.header("User-Agent", "MyApp/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/json")
.build()
chain.proceed(request)
}
.build()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttp)
.addConverterFactory(MoshiConverterFactory.create())
.build()
Gestion d’erreurs centrale qui mappe vers des messages utilisateurs
Les erreurs réseau ne sont pas juste « une exception ». Si chaque écran les traite différemment, les utilisateurs voient des messages incohérents.
Créez un mapper qui convertit les échecs en un petit ensemble de résultats conviviaux : pas de connexion/mode avion, timeout, erreur serveur (5xx), erreur de validation ou d’auth (4xx) et un fallback inconnu.
Cela maintient la cohérence du texte UI (« Pas de connexion » vs « Réessayez ») sans exposer des détails techniques.
Taggez et annulez les requêtes quand les écrans ferment
Sur des réseaux instables, les appels peuvent finir tard et mettre à jour un écran qui a déjà disparu. Faites de l’annulation une règle standard : quand un écran ferme, son travail s’arrête.
Avec Retrofit et les coroutines Kotlin, annuler le scope de coroutine (par exemple dans un ViewModel) annule l’appel HTTP sous‑jacent. Pour les appels non‑coroutines, conservez la référence au Call et appelez cancel(). Vous pouvez aussi tagger les requêtes et annuler des groupes d’appels quand une fonctionnalité est quittée.
Le travail en arrière‑plan ne doit pas dépendre de l’UI
Tout ce qui est important et doit se terminer (envoi d’un rapport, synchronisation d’une file, finalisation d’une soumission) devrait s’exécuter dans un ordonnanceur conçu pour ça. Sur Android, WorkManager est le choix habituel car il peut réessayer plus tard et survivre aux redémarrages. Gardez les actions UI légères et déléguez les travaux longs au background quand c’est pertinent.
Règles de cache sûres pour le mobile
Le cache peut grandement améliorer l’expérience sur des connexions lentes en évitant des téléchargements répétés et en rendant les écrans instantanés. Il peut aussi poser problème s’il affiche des données périmées au mauvais moment, comme un solde ancien ou une adresse de livraison obsolète.
Une approche sûre consiste à ne mettre en cache que ce que l’utilisateur peut tolérer d’avoir un peu vieilli, et à forcer des vérifications fraîches pour tout ce qui affecte l’argent, la sécurité ou une décision finale.
Principes Cache‑Control sur lesquels vous pouvez compter
La plupart des règles se résument à quelques en‑têtes :
max-age=60: vous pouvez réutiliser la réponse mise en cache pendant 60 secondes sans interroger le serveur.no-store: ne pas enregistrer cette réponse du tout (idéal pour tokens et écrans sensibles).must-revalidate: si c’est expiré, vous devez vérifier auprès du serveur avant de l’utiliser à nouveau.
Sur mobile, must-revalidate évite des données « silencieusement erronées » après une période hors‑ligne. Si l’utilisateur ouvre l’app après un trajet en métro, vous voulez un écran rapide, mais aussi que l’app confirme ce qui est encore vrai.
Rafraîchissements ETag : rapides, peu coûteux et fiables
Pour les endpoints en lecture, la validation par ETag est souvent meilleure que de longues valeurs max-age. Le serveur envoie un ETag avec la réponse. Ensuite, l’app envoie If-None-Match avec cette valeur. Si rien n’a changé, le serveur répond 304 Not Modified, qui est minime et rapide sur des réseaux faibles.
Cela fonctionne bien pour les listes de produits, les détails de profil et les écrans de paramètres.
Règles pratiques :
- Mettez en cache les endpoints en
GETavec un courtmax-ageplusmust-revalidate, et supportez lesETagsi possible. - Ne mettez pas en cache les endpoints en écriture (POST/PUT/PATCH/DELETE). Traitez‑les comme toujours dépendants du réseau.
- Utilisez
no-storepour tout ce qui est sensible (réponses d’auth, étapes de paiement, messages privés). - Cachez plus longtemps les assets statiques (icônes, config publique), car le risque de péremption est faible.
Gardez les décisions de cache cohérentes dans l’app. Les utilisateurs remarquent les incohérences plus que de petits délais.
Reprises sûres sans aggraver les choses
Les retries semblent une solution facile, mais elles peuvent se retourner contre vous. Réessayez les mauvaises requêtes et vous créez une charge supplémentaire, videz la batterie et donnez l’impression que l’app est bloquée.
Commencez par ne réessayer que les échecs probablement temporaires. Une connexion tombée, un read timeout ou une courte panne serveur peuvent réussir au prochain essai. Un mauvais mot de passe, un champ manquant ou un 404 ne le feront pas.
Règles pratiques :
- Réessayez les timeouts et les échecs de connexion.
- Réessayez 502, 503, et parfois 504.
- N’essayez pas les 4xx (sauf 408 ou 429 si vous avez une règle d’attente claire).
- N’essayez pas les requêtes qui ont déjà atteint le serveur et peuvent être en cours de traitement.
- Limitez les retries (souvent 1 à 3 tentatives).
Backoff + jitter : moins de vagues de retry
Si de nombreux utilisateurs rencontrent la même panne, des retries instantanés peuvent créer une vague de trafic qui ralentit la récupération. Utilisez un backoff exponentiel (attendre de plus en plus longtemps) et ajoutez du jitter (un petit délai aléatoire) pour que les appareils ne réessaient pas en synchronisation.
Par exemple : attendre environ 0,5 s, puis 1 s, puis 2 s, avec un aléa de ±20% à chaque fois.
Fixez un plafond au temps total de retry
Sans limites, les retries peuvent piéger les utilisateurs dans un spinner pendant des minutes. Choisissez un temps total maximum pour toute l’opération, en incluant les attentes. Beaucoup d’apps visent 10–20 secondes avant d’arrêter et d’afficher une option claire de réessayer.
Adaptez aussi au contexte. Si quelqu’un soumet un formulaire, il veut une réponse rapidement. Si une synchronisation en arrière‑plan échoue, vous pouvez réessayer plus tard.
Ne réessayez jamais automatiquement des actions non‑idempotentes (comme passer une commande ou envoyer un paiement) à moins d’avoir une protection telle qu’une clé d’idempotence ou une vérification serveur des doublons. Si vous ne pouvez pas garantir la sécurité, échouez clairement et laissez l’utilisateur décider de la suite.
Prévention des doublons pour les actions critiques
Sur une connexion lente ou instable, les utilisateurs appuient deux fois. L’OS peut relancer en arrière‑plan. Votre app peut renvoyer après un timeout. Si l’action crée quelque chose (commande, envoi d’argent, changement irréversible), les doublons sont problématiques.
L’idempotence signifie que la même requête doit produire le même résultat. Si la requête est répétée, le serveur ne doit pas créer une seconde commande. Il doit retourner le premier résultat ou indiquer « déjà fait ».
Utilisez une clé d’idempotence pour chaque tentative critique
Pour les actions critiques, générez une clé d’idempotence unique quand l’utilisateur commence la tentative, et envoyez‑la avec la requête (souvent dans un header comme Idempotency-Key, ou dans le corps).
Flux pratique :
- Créez un UUID d’idempotence quand l’utilisateur appuie sur « Payer ».
- Sauvegardez‑le localement avec un petit enregistrement : status = pending, createdAt, hash du payload.
- Envoyez la requête avec la clé.
- À la réussite, marquez status = done et stockez l’ID de résultat renvoyé par le serveur.
- Si vous devez réessayer, réutilisez la même clé, pas une nouvelle.
Cette règle « réutiliser la même clé » empêche les doubles prélèvements accidentels.
Gérer les redémarrages et les interruptions hors‑ligne
Si l’app est tuée en plein envoi, le prochain lancement doit rester sûr. Stockez la clé d’idempotence et l’état de la requête en local (par exemple une ligne de base de données). Au redémarrage, soit réessayez avec la même clé, soit appelez un endpoint « check status » en utilisant la clé sauvegardée ou l’ID de résultat serveur.
Côté serveur, le contrat doit être clair : à la réception d’une clé dupliquée, il doit rejeter la seconde tentative ou retourner la réponse originale (même order ID, même reçu). Si le serveur ne peut pas le faire encore, la prévention client des doublons ne sera jamais entièrement fiable, car l’app ne peut pas savoir ce qui s’est passé après l’envoi.
Une bonne attention côté utilisateur : si une tentative est en cours, afficher « Paiement en cours » et désactiver le bouton jusqu’à un résultat final.
Patterns UI qui réduisent les resoumissions accidentelles
Les connexions lentes ne cassent pas que les requêtes. Elles changent la manière dont les gens tapent. Quand l’écran se fige deux secondes, beaucoup d’utilisateurs supposent que rien ne s’est passé et appuient de nouveau. Votre UI doit faire en sorte qu’un « coup » soit perçu comme fiable même quand le réseau ne l’est pas.
L’UI optimiste est la plus sûre quand l’action est réversible ou à faible risque, comme marquer un item, sauvegarder un brouillon ou marquer un message comme lu. L’UI confirmée est préférable pour l’argent, le stock, les suppressions irréversibles et tout ce qui peut créer des doublons.
Un défaut raisonnable pour les actions critiques est un état « pending » clair. Après le premier tap, changez immédiatement le bouton principal en état « Submitting… », désactivez‑le et affichez une petite ligne expliquant ce qui se passe.
Patterns efficaces sur réseaux instables :
- Désactivez l’action principale après le tap et gardez‑la désactivée jusqu’à un résultat final.
- Affichez un statut visible « Pending » avec des détails (montant, destinataire, nombre d’articles).
- Ajoutez une vue « Activité récente » pour que les utilisateurs confirment ce qu’ils ont déjà envoyé.
- Si l’app est backgroundée, conservez l’état pending quand ils reviennent.
- Privilégiez un bouton primaire clair plutôt que plusieurs cibles sur le même écran.
Parfois la requête réussit mais la réponse est perdue. Traitez‑le comme un cas normal, pas comme une erreur qui incite à réappuyer. Au lieu de « Échoué, réessayer », affichez « Nous n’en sommes pas sûrs » et proposez une action sûre comme « Vérifier le statut ». Si vous ne pouvez pas vérifier, conservez l’enregistrement pending localement et dites à l’utilisateur que vous mettrez à jour quand la connexion revient.
Rendez « Réessayer » explicite et sûr. Ne l’affichez que quand vous pouvez répéter la requête en réutilisant le même ID client ou la même clé d’idempotence.
Exemple réaliste : une soumission de checkout instable
Un client est dans un train avec un signal intermittent. Il ajoute des articles au panier et appuie sur Payer. L’app doit faire preuve de patience, mais elle ne doit pas créer deux commandes.
Une séquence sûre ressemble à ceci :
- L’app crée un attempt ID côté client et envoie la requête de checkout avec une clé d’idempotence (par exemple un UUID stocké avec le panier).
- La requête attend un connect timeout clair, puis un read timeout plus long. Le train entre dans un tunnel et l’appel timeoute.
- L’app réessaie une fois, mais seulement après un court délai et uniquement si elle n’a jamais reçu de réponse serveur.
- Le serveur reçoit la seconde requête et voit la même clé d’idempotence, il retourne donc le résultat original au lieu de créer une nouvelle commande.
- L’app affiche un écran de confirmation final quand elle reçoit la réponse de succès, même si elle vient du retry.
Le caching suit des règles strictes. Les listes de produits, options de livraison et tables fiscales peuvent être mises en cache pour une courte durée (GET). La soumission du checkout (POST) n’est jamais mise en cache. Même si vous utilisez un cache HTTP, traitez‑le comme une aide en lecture pour la navigation, pas comme un moyen de « mémoriser » un paiement.
La prévention des doublons mélange choix réseau et UI. Quand l’utilisateur appuie sur Payer, le bouton est désactivé et l’écran affiche « Soumission de la commande… » avec une seule option Annuler. Si l’app perd le réseau, elle bascule sur « Toujours en cours » et conserve le même attempt ID. Si l’utilisateur force‑ferme et rouvre, l’app peut reprendre en vérifiant le statut de la commande avec cet ID, au lieu de lui demander de payer à nouveau.
Checklist rapide et prochaines étapes
Si votre app fonctionne « à peu près » sur le Wi‑Fi du bureau mais tombe en morceaux dans les trains, ascenseurs ou zones rurales, considérez cela comme un blocage de release. Ce travail concerne moins du code intelligent et plus des règles claires et répétables.
Checklist avant publication :
- Fixez des timeouts par type d’endpoint (login, feed, upload, checkout) et testez sur réseaux bridés et à haute latence.
- Réessayez uniquement là où c’est vraiment sûr, avec un backoff (quelques tentatives pour les lectures, en général aucune pour les écritures).
- Ajoutez une clé d’idempotence pour chaque écriture critique (paiements, commandes, soumissions de formulaire) afin qu’un retry ou un double tap ne puisse pas créer des doublons.
- Rendre explicites les règles de cache : ce qui peut être servi périmé, ce qui doit être frais, et ce qui ne doit jamais être mis en cache.
- Rendre les états visibles : pending, failed et completed doivent être distincts, et l’app doit se souvenir des actions complétées après un redémarrage.
Si l’une de ces décisions est « on décidera plus tard », vous aurez un comportement aléatoire entre les écrans.
Prochaines étapes pour pérenniser
Rédigez une page de politique réseau : catégories d’endpoints, cibles de timeouts, règles de retry et attentes de cache. Faites‑la appliquer en un point unique (interceptors, fabrique de client partagée, ou un petit wrapper) pour que chaque membre de l’équipe obtienne par défaut le même comportement.
Ensuite, faites un court exercice de duplication. Choisissez une action critique (comme le checkout), simulez un spinner gelé, forcez la fermeture de l’app, activez/désactivez le mode avion, et appuyez à nouveau sur le bouton. Si vous ne pouvez pas prouver que c’est sûr, les utilisateurs finiront par casser le flux.
Si vous voulez appliquer les mêmes règles côté backend et client sans tout câbler manuellement, AppMaster (appmaster.io) peut aider en générant du code backend et natif prêt pour la production. Même dans ce cas, l’essentiel reste la politique : définissez idempotence, retries, cache et états UI une fois, et appliquez‑les de manière cohérente sur tout le flux.
FAQ
Commencez par définir ce que « correct » signifie pour chaque écran et action, en particulier tout ce qui doit se produire au maximum une fois comme les paiements ou les commandes. Une fois les règles claires, adaptez les timeouts, les retries, le caching et les états UI pour correspondre à ces règles au lieu de compter sur les valeurs par défaut des bibliothèques.
Les utilisateurs voient généralement des spinners sans fin, des erreurs après une longue attente, des actions qui fonctionnent au second essai, ou des résultats en double comme deux commandes ou des doubles prélèvements. Cela vient souvent de règles floues sur les retries et l’état « en attente vs échoué », pas seulement d’un signal faible.
Utilisez le connect timeout pour le temps d’établissement de la connexion, le write timeout pour l’envoi du corps de la requête (uploads), et le read timeout pour attendre la réponse après l’envoi. Une bonne pratique est d’avoir des timeouts plus courts pour les lectures peu risquées et des read/write plus longs pour les envois critiques, tout en limitant l’attente côté UI pour que l’utilisateur ne reste pas bloqué indéfiniment.
Oui : si vous ne pouvez régler qu’un seul timeout, utilisez callTimeout pour limiter l’opération entière afin d’éviter une attente « infinie ». Ensuite, superposez connect/read/write pour un contrôle plus fin, surtout pour les uploads et les corps de réponse lents.
Commencez par réessayer uniquement les échecs temporaires comme les coupures de connexion, les problèmes DNS et les timeouts, et parfois les 502/503/504. Évitez de réessayer les 4xx et n’auto-réessayez pas les écritures à moins d’avoir une protection d’idempotence, car les retries peuvent générer des doublons.
Utilisez un petit nombre de retries (souvent 1–3) avec un backoff exponentiel et un peu de jitter pour éviter que de nombreux appareils ne réessaient en même temps. Limitez aussi le temps total passé en retries pour que l’utilisateur obtienne rapidement un résultat clair plutôt qu’un spinner qui dure des minutes.
L’idempotence signifie que répéter la même requête ne doit pas créer un second résultat : un double tap ou un retry ne doit pas entraîner un double débit ou une double réservation. Pour les actions critiques, envoyez une clé d’idempotence par tentative et réutilisez-la pour les retries afin que le serveur retourne le même résultat initial au lieu d’en créer un nouveau.
Générez une clé unique au démarrage de l’action, stockez-la localement avec un petit enregistrement « pending », et envoyez-la avec la requête. Si vous réessayez ou si l’application redémarre, réutilisez la même clé et soit réessayez en toute sécurité, soit appelez un endpoint de vérification de statut pour éviter de transformer une seule intention utilisateur en deux écritures serveur.
Mettez en cache uniquement les données qui peuvent être un peu vieillies sans problème, et forcez des vérifications fraîches pour l’argent, la sécurité et les décisions finales. Pour les lectures, préférez une courte durée de fraîcheur avec revalidation et envisagez les ETag ; pour les écritures, ne mettez rien en cache, et utilisez no-store pour les réponses sensibles.
Désactivez le bouton principal après le premier tap, affichez immédiatement un état « Submitting… », et conservez un statut visible « pending » qui survive au passage en arrière-plan ou aux redémarrages. Si la réponse peut être perdue, n’incitez pas l’utilisateur à retaper : affichez plutôt une incertitude (« Nous n’en sommes pas sûrs pour le moment ») et proposez une action sûre comme « Vérifier le statut ».


