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, a small runtime, two dependencies (http and meta).
- 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 normalized: each object is stored once (
Launch:launch-181), and only the widgets that read a field that changed 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'); LaunchStatus? get status => enumValue('status', LaunchStatus.fromGraphQL); Rocket? get rocket => object('rocket', Rocket.new, keyed: true); List<Payload>? get payloads => list('payloads', Payload.new, keyed: true);}Arguments become named parameters, enums become Dart enums, input objects become
const classes with toJson(). Regenerate in one command:
dart run sling_gql_gen –endpoint …
Status
Queries, the normalized cache, mutations, subscriptions and unions are solid; the cache persists in SQLite through a separate adapter. From 1.0 every package follows a semver and deprecation policy.
| 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 (client.subscribe, SubscriptionBuilder, SSE transport) |
done |
Unions / interfaces (asLaunch, when(...), inline fragments) |
done |
Fetch policies, maxAge stale-while-revalidate |
done |
Persistence (sling_gql_sqflite: SQLite, deltas, bounded) |
done |