sling_gql 1.0.0
sling_gql: ^1.0.0 copied to clipboard
GraphQL client for Flutter where the widget is the query — read a field, get the query.
sling_gql #
A GraphQL client for Flutter where the widget is the query. Inspired by GQty.
Docs: https://tpucci.github.io/sling_gql/ · Source: https://github.com/tpucci/sling_gql
Read a field, get the query. No operation documents, no fragments, no builder-per-query boilerplate: widgets read typed accessors during
build(), sling_gql records what was read, batches everything read in the frame into one GraphQL document, fetches it, fills a normalized cache, and rebuilds only the widgets that read the affected data.
QueryBuilder<Query>(
builder: (context, query, state) {
final launch = query.latestLaunch;
return ListTile(
title: Text(launch?.name ?? '…'),
subtitle: Text(launch?.rocket?.name ?? '…'),
);
},
)
…sends, once, at the end of the frame:
query {
latestLaunch { __typename name rocket { __typename name } }
}
Getting started #
-
Add the runtime and the generator:
flutter pub add sling_gql flutter pub add --dev sling_gql_gen -
Generate typed accessors from your schema's introspection JSON (the standard
{"__schema": …}result):dart run sling_gql_gen --schema graphql/schema.json \ --out lib/generated/schema.dart --scalar DateTime=DateTime -
Create a client and provide it to the tree:
import 'package:sling_gql/sling_gql.dart'; import 'generated/schema.dart'; void main() { final client = SlingClient<Query>( endpoint: Uri.parse('https://example.com/graphql'), schema: slingSchema, // generated: roots + key field ); runApp(SlingScope<Query>( client: client, child: const MyApp(), )); } -
Read fields in
build()withQueryBuilder; mutate withMutationBuilderorclient.mutate((m) => m.toggleFavorite(id: id)?.favorite); subscribe withSubscriptionBuilderorclient.subscribe((s) => s.launchStatusChanged?..status). -
Test with
sling_gql_test:MockGraphQLServeranswers the recorded documents from plain maps,tester.pumpUntilSettled(client)waits for the round trip.
What's in the box #
-
Selection recording during
build(), including accessors handed to child widgets and lazily built sliver rows. -
One HTTP request per frame, arguments turned into variables.
-
Normalized cache (
Launch:launch-181), per-field rebuild notifications (only on real changes, inline objects included),evict,gc,snapshot/onChange/changesSince(incremental deltas) for persistence. -
SlingRow: a list row with its own dependencies, so a write to one entity rebuilds that row instead of the whole list. -
Skeleton state (
nullscalars, one-element lists) before data arrives;state.isSkeleton,state.isLoading,state.error,state.refetch(). -
prepare:to fetch fields hidden behind conditionals in the first round trip; dev-mode waterfall warnings. -
Request log:
client.requests(SlingRequest: fields, duration, bytes, error, the widgets that asked for it),logRequests: truefor one console line per operation,SlingRequestOverlayfor the same list in the app. -
Mutations with optimistic writes (journaled, rolled back on failure).
-
Offline mutations (
offline: true): queued while the server is unreachable, replayed in order, persisted through aMutationQueueStore. -
A sealed error model (
SlingNetworkException,SlingHttpException,SlingGraphQLExceptionwith error codes, …),errorPolicy(none / all / ignore), sticky errors, retry cooldown. -
Auth with single-flight token refresh (
SlingClient(auth: SlingAuth(...))), query retry with backoff (RetryPolicy), timeouts, cancellation of requests nobody waits for. -
Fetch policies per widget (
cacheFirst,cacheAndNetwork,networkOnly) andmaxAgestale-while-revalidate withstate.isStale/revalidate(). -
Type policies (
SlingClient(typePolicies:)): key arguments and custom merges per field;RelayStylePaginationkeeps a cursor connection in one growing cached list, read byPaginatedQueryBuilder.CacheScope.listfor list membership. -
Subscriptions over Server-Sent Events (
client.subscribe,SubscriptionBuilder), each event normalized into the cache;subscriptionTransport:for another protocol. -
Unions and interfaces: generated
asLaunchgetters (inline fragments) andwhen(launch: …, rocket: …), all branches recorded on the skeleton.
Rules of thumb #
- Read every field you need at the top of
build. A field read only inside anifon fetched data or in a callback costs a second round trip. Useprepare:for the rest. - All generated getters are nullable, even for
!schema types: a value can always be absent from cache. Tell a servernullfrom "not fetched yet" withstate.hasMissingData.
See the documentation site for the
full guide and the example/ app in the repository.
Versioning #
From 1.0 the sling_gql packages follow semantic versioning and share a major
version. The public API is what package:sling_gql/sling_gql.dart exports,
minus members marked @internal (runtime machinery) or @protected
(Accessor's helpers, for generated code). package:sling_gql/internal.dart
holds the building blocks first-party adapters use: no stability
promise, it can change in any minor. Code generated by sling_gql_gen 1.y
runs on sling_gql 1.x for every x ≥ y; a newer generator may need a newer
runtime (its changelog says which). Deprecated APIs keep working for at least one minor release and are
removed only in the next major. Full policy:
Versioning & deprecation; coming from 0.x:
Upgrading to 1.0. Security issues:
SECURITY.md.