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.
Scalars and nested objects
Section titled “Scalars and nested objects”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}'), ]); },)query { company { __typename name headquarters { __typename city country } }}Three things to notice:
- Every object selection carries
__typename, and keyed types also carryid. 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 saysCompany!but the cache may not have it yet.nullhere means “not fetched”; a servernullwould also benull. Usestate.hasMissingDataorcompany.isSkeletonwhen you need to tell them apart.- Nothing is fetched twice. Reading
company.namein three widgets produces onenamein 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')],);query { rockets { __typename name stages }}Lists of scalars ([String!]!) come back as List<String?>? via scalarList:
final customers = payload.customers?.join(', ');Arguments
Section titled “Arguments”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,);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 # … }}{ "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.
Input objects
Section titled “Input objects”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.
Reusing selections (“fragments”)
Section titled “Reusing selections (“fragments”)”A function that reads fields is a fragment. Call it from any widget or from prepare:
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.
Reading outside of build()
Section titled “Reading outside of build()”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.
Imperative refetch
Section titled “Imperative refetch”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()).