Swelv· Part 2 of 2
Flutter and GraphQL: a typed client, and versions that coexist
A client generated from the schema, screens that ask for exactly their fields, and a server that keeps talking to the versions already installed.
5 min readLire en français
On the web, a deploy updates everyone. On mobile, it does not: every published version stays installed on phones for weeks, sometimes months, and they all talk to the same server.
Why Flutter, why GraphQL
Swelv runs on iOS and Android, with a small team. Flutter lets you write the app once, in Dart, and compile it for both platforms, with smooth animations and the same rendering everywhere. A feature is built once, tested once, and ships the same day on both stores.
On the API side, GraphQL solves a typical mobile problem. With a REST API, each screen gets what the endpoint was built to return: too much data, or not enough, and a second call. With GraphQL, there is one entry point, and each screen describes exactly the fields it wants, in a single round trip. On a mobile network, you feel it.
Too much data, and two round trips.
book {
title author cover
friends { name photo }
}titleauthorcovernamephotoExactly what the screen shows, in one round trip.
The challenge. Keep changing the API, without ever breaking an app already installed that its owner will not update for a long time.
A typed, generated client
The GraphQL schema describes every type the server can serve. It is generated from the server's code, then the app uses it to generate its own Dart classes. No more hand-read JSON: every request is a class, every field has its type, and a typo does not compile.
query Home { books { title author cover }}
final res = await client .request(GHomeReq()) .first;final book = res.data?.books.first;book?.title; // Stringbook?.price; // ✗ does not compile
Each screen asks for its fields
This is what makes GraphQL so valuable on mobile: a version only gets what it asks for. An added field does not bother it; it never even sees it. And on the server, only the requested columns are read from the database.
Home
★★★★☆
books { title cover}
Home
★★★★☆
books { title cover rating}
The trap: enum values
Adding a field is safe. Adding a value to a closed list (an enum) is not: an old version's generated code only knows the values of its time, and an unknown value makes the whole response fail. So the app sends its version with every request, and the server adapts its answers for versions that are too old.
The same response, for two versions
the app sends its version{ "status": "PAUSED", "moods": ["MOVED", "AMUSED"]}
{ "status": "READING", "moods": ["MOVED"]}
The old app shows less, but it shows.
Add, deprecate, and only then remove
Removing something from the API removes it from every installed version at once. Hence a rule in three steps:
- 01AddAn optional field or query: safe, nobody asks for it yet.
- 02DeprecateThe replacement next to it, the old one marked deprecated, and kept.
- 03RemoveOnly once no installed version uses it: a required update, or version statistics.
One request lives outside the generated client: the one that asks whether the app should update. It must work even when everything else has changed.
What I take from it
- Generate the client from the schema. The compiler reviews every API change.
- An installed version is a client you no longer control. Send it what it understands.
- Adding is free; removing has to be earned.