Why sling_gql?
The operation-first tax
Section titled “The operation-first tax”Every mainstream GraphQL client for Flutter — Ferry, graphql_flutter, Artemis — is
operation-first. You write a .graphql document, run a generator, get a typed request
class and a typed response class, then wire a widget to that request:
query Launches($first: Int) { launches(first: $first) { nodes { id name date rocket { name } } }}Operation( client: client, operationRequest: GLaunchesReq((b) => b..vars.first = 20), builder: (context, response, error) { if (response.loading) return const Spinner(); final launches = response.data?.launches.nodes ?? []; return ListView(children: [for (final l in launches) Text(l.name)]); },)This is safe and explicit, but it has costs you pay on every screen:
- Two sources of truth. The document says what is fetched; the widget says what is
displayed. They drift. You over-fetch to be safe, or under-fetch and crash on
null. - Fragments as a chore. Sharing selections between a list row and a detail page means fragments, fragment spreads, and generated fragment types you have to thread around.
- Loading is a branch.
if (loading)doubles your widget code and your test cases. - Batching needs server support. Two widgets, two requests — unless your server supports batching and your client knows how to use it.
The GQty idea
Section titled “The GQty idea”GQty inverted this for React: the component is the query. You read
me.name from a proxy during render, the proxy records the access, and after render the
recorded fields are sent as one document. The cache fills, the component re-renders with
data.
It has real, measurable benefits:
- Nothing to keep in sync. What you display is what you fetch.
- Fragments are functions. A
prepare(launch)that reads fields is reusable anywhere. - Loading is a value. Missing data is
undefined; you render a skeleton with??. - Batching is free. Every component rendered in the same tick contributes to one request, with no server cooperation.
It also has a well-known cost — waterfalls on conditional reads — which we address head-on in Batching & waterfalls.
sling_gql takes that idea and asks what it looks like in Dart, on Flutter’s frame model, with sound null safety. It is not a port of the JavaScript library; the API and the internals are designed for Flutter from scratch.
Why it seemed impossible in Dart
Section titled “Why it seemed impossible in Dart”The JavaScript approach depends on Proxy to intercept arbitrary property access on
objects whose shape is only known from the schema. Dart has no Proxy. noSuchMethod
exists but fights sound null safety and disables AOT optimisations.
The answer sling_gql explores is: generate the interception. One Dart class per GraphQL type, one getter per field. Each getter does exactly what a proxy would have done — record the field on a selection tree, then read from the cache. It is static, typed, and tree-shakeable — and you were going to run a generator for your types anyway.
class Launch extends Accessor { Launch(super.recorder, super.selection, super.path);
String? get name => scalar<String>('name'); set name(String? v) => write('name', v); Rocket? get rocket => object('rocket', Rocket.new); List<Payload>? get payloads => list('payloads', Payload.new);}What the proof of concept set out to prove
Section titled “What the proof of concept set out to prove”- Recording during
build()is reliable — including fields read by child widgets, by lazily built sliver rows, and after the builder returned. - One request per frame is achievable without server-side batching.
- The API is pleasant in Dart — nullable getters, named arguments,
prepareas a plain function. - The runtime stays small — no
gql, noferry, nobuild_runner; one HTTP dependency.
All four held, with two surprises (frame timing and partial errors) documented in the feasibility notes.
What it does not try to prove (yet)
Section titled “What it does not try to prove (yet)”Subscriptions, unions, persistence. They are on the roadmap, and the bundled mock API already exposes mutations and subscriptions so the next phases have something to talk to.