Skip to content

Querying data

Querying in sling_gql is reading. This page walks through the generated API for the mock API’s schema, showing the GraphQL each snippet produces.

QueryBuilder<Query>(
builder: (context, query, state) {
final company = query.company;
final hq = company?.headquarters;
return Column(children: [
Text(company?.name ?? '…'),
Text('${hq?.city}, ${hq?.country}'),
]);
},
)
generated
query {
company {
__typename
name
headquarters {
__typename
city
country
}
}
}

Three things to notice:

  • Every object selection carries __typename, and keyed types also carry id. Both are added by the printer; together they form the entity key (Launch:launch-181) the normalized cache stores objects under.
  • company?. is nullable because the schema says Company! but the cache may not have it yet. null here means “not fetched”; a server null would also be null. Use state.hasMissingData or company.isSkeleton when you need to tell them apart.
  • Nothing is fetched twice. Reading company.name in three widgets produces one name in one document.

Lists are Dart lists. Before data arrives they contain exactly one skeleton element, so your map/for runs once and records the element’s fields.

final rockets = query.rockets ?? const <Rocket>[];
return Column(
children: [for (final r in rockets) Text('${r.name} · ${r.stages} stages')],
);
generated
query {
rockets {
__typename
name
stages
}
}

Lists of scalars ([String!]!) come back as List<String?>? via scalarList:

final customers = payload.customers?.join(', ');

Fields with arguments are methods with named parameters. Required arguments (ID! without a default) are required; everything else is optional.

final launch = query.launch(id: widget.id);
final page = query.launches(
first: 20,
filter: LaunchFilter(status: LaunchStatus.SUCCESS, year: 2024),
orderBy: LaunchOrder.DATE_DESC,
);
generated
query ($id: ID!, $first: Int, $filter: LaunchFilter, $orderBy: LaunchOrder) {
launch_18di94z: launch(id: $id) {
__typename
# …
}
launches_5c0z2wq: launches(first: $first, filter: $filter, orderBy: $orderBy) {
__typename
# …
}
}
variables
{ "id": "launch-181", "first": 20, "filter": { "status": "SUCCESS", "year": 2024 }, "orderBy": "DATE_DESC" }

Variables are named after the argument ($first, $after, $filter); if the same argument name is used twice with a different value in one operation, the second (and later) distinct value gets a numeric suffix ($first, $first2).

Arguments always travel as variables, so input objects and enums need no client-side serialization beyond toJson(), which the generated input classes implement. The alias (launches_5c0z2wq) is a stable hash of the argument values and is also the cache key: launches(first: 20) and launches(first: 40) are different cache entries that can coexist in one document. This is what makes pagination fall out naturally.

final filter = LaunchFilter(
search: 'Starlink',
upcoming: false,
);
query.launchesPage(limit: 10, offset: 0, filter: filter);

LaunchFilter is a plain const class with a toJson() that omits nulls. Nested inputs and lists of inputs are serialized recursively.

A function that reads fields is a fragment. Call it from any widget or from prepare:

launch_screen.dart (excerpt)
static void prepare(Launch launch) {
launch
..name
..date
..status
..details;
launch.rocket
?..name
..description
..successRatePct;
for (final p in launch.payloads ?? const <Payload>[]) {
p..name..type$..orbit;
}
}

Cascade syntax (..) reads a getter for its side effect; the analyzer is fine with it because the getters are real members. See Batching & waterfalls for when to reach for prepare.

Sometimes you need data imperatively — a prefetch, a test, a callback. SlingClient.resolve runs a selection function against a throwaway scope, fetches what is missing, and returns the result once the cache is populated:

final name = await client.resolve((q) => q.latestLaunch?.name);

resolve batches on a microtask instead of a frame, so it also works in pure Dart tests.

state.refetch() re-fetches everything the current widget selected during its last build — the natural thing to hook to pull-to-refresh:

CupertinoSliverRefreshControl(onRefresh: state.refetch)

Because all pages of a paginated list live in the same widget’s selection, one pull refetches all loaded pages in one request.

To refresh on open, or automatically after a while, see Fetch policies & freshness (fetchPolicy:, maxAge, state.revalidate()).