Getting started
-
Add the runtime and the generator.
Terminal window flutter pub add sling_gqlflutter pub add --dev sling_gql_genBoth are on pub.dev (
sling_gql,sling_gql_gen) and released in lockstep. To trackmaininstead, use agit:dependency withurl: https://github.com/tpucci/sling_gqlandpath: packages/sling_gql(resp.packages/sling_gql_gen). -
Generate the typed accessors from your endpoint.
Terminal window dart run sling_gql_gen \--endpoint https://api.example.com/graphql \--out lib/generated/schema.dartAdd
-H "Authorization: Bearer …"if introspection needs a header, or pass--schema introspection.jsonif you already have the introspection result on disk.You get one class per GraphQL type, a constant holder per enum and a Dart class per input object — see Code generation for the rules. Re-run the command whenever your schema changes; the compiler will tell you what moved.
-
Create a client and provide it to the tree.
lib/main.dart import 'package:flutter/cupertino.dart';import 'package:sling_gql/sling_gql.dart';import 'generated/schema.dart';void main() {final client = SlingClient<Query>(endpoint: Uri.parse('https://api.example.com/graphql'),rootFactory: Query.root,headers: {'Authorization': 'Bearer …'}, // optional; see TransportonOperation: (op) => debugPrint(op.document), // optional: log every request);runApp(SlingScope<Query>(client: client,child: const CupertinoApp(home: HomeScreen()),));} -
Read fields inside a
QueryBuilder.lib/home_screen.dart class HomeScreen extends StatelessWidget {const HomeScreen({super.key});@overrideWidget build(BuildContext context) {return QueryBuilder<Query>(builder: (context, query, state) {final launch = query.latestLaunch;return CupertinoPageScaffold(child: Center(child: Text('${launch?.name ?? '…'} on ${launch?.rocket?.name ?? '…'}'),),);},);}}That is the whole integration. There is no query document anywhere in your code: the first build records
latestLaunch { name rocket { name } }, sling_gql sends it once the frame is done, the cache fills, and the builder runs again with real values. -
Handle loading and errors where you need to.
builder: (context, query, state) {if (state.error != null) {return ErrorView(error: state.error!, onRetry: state.refetch);}final launch = query.latestLaunch;return Stack(children: [LaunchCard(launch), // renders skeletons while nullif (state.isLoading) const CupertinoActivityIndicator(),]);}
null means three things
Section titled “null means three things”Every accessor getter is nullable, even for schema fields marked !. null on its own is
ambiguous — it can mean three different things, and telling them apart is the first habit
to build:
| Meaning | How you get it | How to tell |
|---|---|---|
| Not fetched yet | The field has not been read from the server yet (first paint, or a field you have not selected). | state.hasMissingData (whole widget) or accessor.isFetched('field') (one field) is false. |
Server null |
The server answered and the field is genuinely absent (a nullable field with no value). | state.hasMissingData is false and accessor.isFetched('field') is true, yet the getter still returns null. |
| Errored path | The field’s path failed with a GraphQL error; sling_gql never caches a null from an errored path as real data. |
state.error is set. It stays set (see Loading states & errors) until you call state.refetch(). |
The blessed pattern is: gate the whole widget on state.hasMissingData for the
first-paint skeleton (or the shorthand state.isSkeleton, which is exactly
hasMissingData && isLoading), and reach for accessor.isFetched('field') only when a
single field — not the whole screen — needs to tell “not fetched” apart from “the server
said null”:
builder: (context, query, state) { if (state.isSkeleton) return const SkeletonScreen(); // first paint only final launch = query.latestLaunch; final hasDetails = launch.isFetched('details'); // per-field, no extra rebuild return LaunchCard(launch, showDetailsPlaceholder: !hasDetails);}Rules of the road
Section titled “Rules of the road”Four habits cover almost every mistake sling_gql’s request-counting tests catch:
- Read every field the widget might need at the top of
build(), into locals — never only inside anifon fetched data or a callback. See Batching & waterfalls. - Use
preparefor reads that are legitimately conditional (a collapsed section, a hidden tab) so they are still fetched with the first request. Seeprepare: fetching for the future. - Never branch on list length while
state.hasMissingData— a skeleton list always has one element. See the Aside above and Loading states & errors. - Errors are sticky until
refetch()— a failing query never retries on its own. See Loading states & errors.
Where to go next
Section titled “Where to go next”- Querying data — arguments, lists, nesting, and what each read becomes on the wire.
- Batching & waterfalls — the one habit to
build: read every field you will need at the top of
build(). - Example app — a complete iOS app against the bundled mock API, with a test that asserts request counts.