Read a field.
Get the query.
You write the widget. sling_gql writes the GraphQL.
Every getter you touch during build() is recorded. At the end of the frame,
everything read by every widget becomes one request. The response fills a cache and
the widgets that read it rebuild.
QueryBuilder<Query>( builder: (context, query, state) { final launch = query.latestLaunch; return ListTile( title: Text(launch?.name ?? '…'), subtitle: Text(launch?.rocket?.name ?? '…'), ); },)query { latestLaunch { __typename name rocket { __typename name } }}How it works
Four moving parts, ~600 lines of runtime, one dependency (http).
- Generated accessors, not proxies
One Dart class per GraphQL type. Every getter records the field on a selection tree, then reads the cache. Static, typed, tree-shakeable.
- Skeletons instead of loading branches
Before data arrives, scalars are
nulland lists have one placeholder item. Your layout renders once, with the same code path. - One request per frame
Selections from every widget — including lazily built list rows — merge into a single document flushed at the end of the frame.
- Targeted rebuilds
The response is deep-merged into a path-addressed cache; only widgets that read the affected fields are asked to rebuild.
A typed API, generated from your schema
One Dart class per GraphQL type. Every field is a real member — autocomplete, jump to definition, doc comments from the schema. Every field is nullable, because it may not be fetched yet: the compiler makes you handle the skeleton state instead of discovering it at runtime.
type Launch { id: ID! name: String! details: String status: LaunchStatus! rocket: Rocket! payloads: [Payload!]!}class Launch extends Accessor { String? get id => scalar<String>('id'); String? get name => scalar<String>('name'); set name(String? v) => write('name', v); String? get details => scalar<String>('details'); String? get status => scalar<String>('status'); Rocket? get rocket => object('rocket', Rocket.new); List<Payload>? get payloads => list('payloads', Payload.new);}Arguments become named parameters, enums become constants, input objects become
const classes with toJson(). Regenerate in one command:
dart run sling_gql_gen –endpoint …
Proof-of-concept status
Queries, the normalized cache and mutations are solid. Subscriptions and unions are the roadmap, not a promise.
| Capability | Status |
|---|---|
Selection recording during build(), incl. child widgets and lazy slivers |
done |
| End-of-frame batching across widgets | done |
| Arguments → variables, per-args aliases & cache keys | done |
Skeleton state, prepare, refetch, sticky errors, partial errors[] |
done |
| Optimistic writes | done |
| Code generator (objects, enums, inputs, custom scalars) | done |
Normalized cache (__typename + id), entity lookups, per-field rebuilds |
done |
Mutations (client.mutate, MutationBuilder, optimistic + rollback) |
done |
| Subscriptions (mock API already exposes them over SSE) | next |
Unions / interfaces ($on) |
not started |
Fetch policies, maxAge stale-while-revalidate |
done |
| Persistence adapters | not started |