Skip to content

Getting started

  1. Add the runtime and the generator.

    Terminal window
    flutter pub add sling_gql
    flutter pub add --dev sling_gql_gen

    Both are on pub.dev (sling_gql, sling_gql_gen) and released in lockstep. To track main instead, use a git: dependency with url: https://github.com/tpucci/sling_gql and path: packages/sling_gql (resp. packages/sling_gql_gen).

  2. Generate the typed accessors from your endpoint.

    Terminal window
    dart run sling_gql_gen \
    --endpoint https://api.example.com/graphql \
    --out lib/generated/schema.dart

    Add -H "Authorization: Bearer …" if introspection needs a header, or pass --schema introspection.json if 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.

  3. 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 Transport
    onOperation: (op) => debugPrint(op.document), // optional: log every request
    );
    runApp(SlingScope<Query>(
    client: client,
    child: const CupertinoApp(home: HomeScreen()),
    ));
    }
  4. Read fields inside a QueryBuilder.

    lib/home_screen.dart
    class HomeScreen extends StatelessWidget {
    const HomeScreen({super.key});
    @override
    Widget 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.

  5. 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 null
    if (state.isLoading) const CupertinoActivityIndicator(),
    ]);
    }

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);
}

Four habits cover almost every mistake sling_gql’s request-counting tests catch:

  1. Read every field the widget might need at the top of build(), into locals — never only inside an if on fetched data or a callback. See Batching & waterfalls.
  2. Use prepare for reads that are legitimately conditional (a collapsed section, a hidden tab) so they are still fetched with the first request. See prepare: fetching for the future.
  3. Never branch on list length while state.hasMissingData — a skeleton list always has one element. See the Aside above and Loading states & errors.
  4. Errors are sticky until refetch() — a failing query never retries on its own. See Loading states & errors.
  • 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.