Aller au contenu
Tous les articles

Swelv· Article 2 sur 2

Flutter et GraphQL : un client typé, et des versions qui cohabitent

Un client généré depuis le schéma, des écrans qui demandent exactement leurs champs, et un serveur qui continue de parler aux versions déjà installées.

5 min de lectureRead in English

Sur le web, un déploiement met tout le monde à jour. Sur mobile, non : chaque version publiée reste installée sur des téléphones pendant des semaines, parfois des mois, et toutes parlent au même serveur.

Pourquoi Flutter, pourquoi GraphQL

Swelv existe sur iOS et Android, avec une petite équipe. Flutter permet d'écrire l'app une seule fois, en Dart, et de la compiler pour les deux plateformes, avec des animations fluides et le même rendu partout. Une fonctionnalité se développe une fois, se teste une fois, et sort le même jour sur les deux stores.

Côté API, GraphQL répond à un problème typique du mobile. Avec une API REST, chaque écran reçoit ce que l'endpoint a prévu de renvoyer : trop de données, ou pas assez, et un deuxième appel. Avec GraphQL, il n'y a qu'un point d'entrée, et chaque écran décrit exactement les champs qu'il veut, en un seul aller-retour. Sur un réseau mobile, ça se sent.

REST2 appels
GET livretitreauteurcouverturerésumééditeurpageslangue
GET amisprénomphotobioville

Trop de données, et deux allers-retours.

GraphQL1 requête
livre {
  titre auteur couverture
  amis { prénom photo }
}
titreauteurcouvertureprénomphoto

Exactement ce que l'écran affiche, en un aller-retour.

Un écran affiche un livre et les amis qui l'ont lu. En REST, chaque endpoint renvoie ce qu'il a prévu : trop de champs, et un second appel. En GraphQL, l'écran décrit exactement ce qu'il affiche. En bleu, ce que l'écran utilise vraiment.

Le défi. Faire évoluer l'API sans arrêt, sans jamais casser une app déjà installée que son propriétaire ne mettra pas à jour avant longtemps.

Un client typé, généré

Le schéma GraphQL décrit tous les types que le serveur sait servir. Il est généré depuis le code du serveur, puis l'app s'en sert pour générer à son tour ses classes Dart. Plus de JSON lu à la main : chaque requête est une classe, chaque champ a son type, et une faute de frappe ne compile pas.

01Le serveur décrit ses typesTypeScript
02Le schémagénéré
03Les requêtes de l'app.graphql
04Le client typéDart, généré
accueil.graphql
query Accueil {  livres {    titre    auteur    couverture  }}
accueil.dart
final res = await client    .request(GAccueilReq())    .first;final livre = res.data?.livres.first;livre?.titre; // Stringlivre?.prix;  // ✗ ne compile pas
Exemple. Le schéma est écrit une fois, côté serveur ; tout le reste en découle. Un champ renommé sur le serveur casse la compilation de l'app, pas l'app d'un lecteur.

Chaque écran demande ses champs

C'est ce qui rend GraphQL précieux sur mobile : une version ne reçoit que ce qu'elle demande. Un champ ajouté ne la dérange pas, elle ne le voit même pas. Et côté serveur, on ne lit en base que les colonnes réellement demandées.

Une version d'avant
9:41

Accueil

★★★★☆

livres {  titre  couverture}
Le serveurun schéma, des requêtes différentesSQL : seulement les colonnes demandées
La version actuelle
9:41

Accueil

★★★★☆

livres {  titre  couverture  note}
Deux versions installées, un seul serveur. Chacune envoie sa propre requête et reçoit exactement ses champs ; le serveur ne lit en base que les colonnes demandées. Un champ ajouté est toujours facultatif : l'ancienne version ne le demande pas, donc ne le voit pas.

Le piège : les valeurs d'enum

Ajouter un champ est sans risque. Ajouter une valeur à une liste fermée (une enum) ne l'est pas : le code généré d'une ancienne version ne connaît que les valeurs de son époque, et une valeur inconnue fait échouer toute la réponse. L'app envoie donc sa version à chaque requête, et le serveur adapte ce qu'il répond aux versions trop anciennes.

La même réponse, pour deux versions

l'app envoie sa version
Version récente
{  "statut": "EN_PAUSE",  "émotions": ["ÉMU", "AMUSÉ"]}
Version ancienne · réponse adaptée
{  "statut": "EN_COURS",  "émotions": ["ÉMU"]}
~ valeur remplacée · − valeur retirée

L'ancienne app affiche moins, mais elle affiche.

Exemple. Une nouvelle valeur, « en pause », et deux nouvelles émotions. La version récente les affiche ; la version ancienne échouerait sur la réponse entière. Le serveur, qui connaît la version de l'app, lui répond donc avec ce qu'elle comprend.

Ajouter, déprécier, puis seulement retirer

Retirer quelque chose de l'API, c'est le retirer à toutes les versions installées en même temps. D'où une règle en trois temps :

  1. 01AjouterUn champ ou une requête facultatifs : sans risque, personne ne les demande encore.
  2. 02DéprécierLe remplaçant à côté, l'ancien marqué comme déprécié, et gardé.
  3. 03RetirerSeulement quand plus aucune version installée ne s'en sert : mise à jour imposée, ou statistiques de versions.
La sortie de secours

Une seule requête vit hors du client généré : celle qui demande s'il faut mettre l'app à jour. Elle doit fonctionner même quand tout le reste a changé.

La règle vient d'un vrai incident : une requête retirée trop tôt a cassé, du jour au lendemain, les versions qui s'en servaient encore. Elle est revenue, dépréciée, et attend que plus personne ne l'utilise.

Ce que j'en retiens

  • Générer le client depuis le schéma. Le compilateur relit chaque changement d'API.
  • Une version installée est un client qu'on ne contrôle plus. On lui envoie ce qu'il comprend.
  • Ajouter est gratuit, retirer se mérite.