Hooks
If your app is built with flutter_hooks,
sling_gql_hooks gives you the three builders as hooks. Nothing else changes:
each hook runs on the same client machinery as its builder — one QueryScope
per widget, the frame’s batched request, per-field rebuilds, sticky errors,
fetch policies — and hands out the same state types.
flutter pub add sling_gql_hooks flutter_hooks| Builder | Hook | Returns |
|---|---|---|
QueryBuilder<Query> |
useSlingQuery((Query q) => …) |
(value, QueryState) |
MutationBuilder<Mutation> |
useSlingMutation<Mutation>() |
(mutate, MutationState) |
SubscriptionBuilder<Subscription> |
useSlingSubscription<Subscription>((s) => …) |
(Subscription?, SubscriptionState) |
All three need the usual SlingScope above (with schema: slingSchema, or a
client built from it, for the mutation and subscription roots).
useSlingQuery
Section titled “useSlingQuery”class LaunchTile extends HookWidget { const LaunchTile({super.key});
@override Widget build(BuildContext context) { final (launch, state) = useSlingQuery( (Query q) => q.latestLaunch?..name..rocket?.name, ); if (state.error != null) { return TextButton(onPressed: state.refetch, child: const Text('Retry')); } return ListTile( title: Text(launch?.name ?? '…'), subtitle: Text(launch?.rocket?.name ?? '…'), ); }}The function you pass — the selector — is what QueryBuilder’s builder
is. It runs in the widget’s scope on every build; every field it reads is
recorded, joins the frame’s request (flushed at the end of the frame, so a
screen of hook widgets and builders still sends one document) and becomes a
dependency: the widget rebuilds when a response, a mutation, a subscription
event or an optimistic write touches it, and only then.
What it returns is up to you: an accessor (the cascade above reads name and
rocket.name and returns the launch), or a record of values:
final ((name, age), state) = useSlingQuery((Query q) => (q.me.name, q.me.age));The named parameters are QueryBuilder’s and, like there, are read when the
scope is created:
final (launches, state) = useSlingQuery( (Query q) => q.launches(first: 20)?.nodes, fetchPolicy: FetchPolicy.cacheAndNetwork, // see Fetch policies maxAge: const Duration(minutes: 5), debugLabel: 'Launches', // request overlay, waterfall warnings);debugLabel defaults to the widget’s key, then (debug builds) the type of the
HookWidget calling the hook (inside a HookBuilder, the enclosing widget’s
type, like the builders). scheduler: is there as well, for the rare
widget that must not wait for the frame (see
Batching & waterfalls).
state is the regular QueryState: isLoading, hasMissingData,
isSkeleton, isStale, error, refetch(), revalidate() — see
Loading states & errors.
useSlingMutation
Section titled “useSlingMutation”class FavoriteButton extends HookWidget { const FavoriteButton({super.key, required this.launch}); final Launch launch;
@override Widget build(BuildContext context) { final id = launch.id; final favorite = launch.favorite ?? false; final (mutate, state) = useSlingMutation<Mutation>(); return CupertinoButton( onPressed: state.isLoading || id == null ? null : () => mutate( (m) => m.toggleFavorite(launchId: id)?.favorite, optimistic: () => launch.favorite = !favorite, ), child: Icon(favorite ? CupertinoIcons.heart_fill : CupertinoIcons.heart), ); }}Exactly MutationBuilder’s contract (see Mutations):
mutate records what the body reads, sends it at once, normalizes the
response into the cache — every widget showing the launch rebuilds — and
resolves to the body’s value, or to null on failure (it never throws; the
exception is on state.error and the optimistic write is rolled back).
state describes the latest call. mutate is the same function on every
build, so it is safe as a useCallback / useEffect key. Pass root: to
use a mutation root other than the SlingScope’s.
useSlingSubscription
Section titled “useSlingSubscription”class LiveStatus extends HookWidget { const LiveStatus({super.key});
@override Widget build(BuildContext context) { final (s, state) = useSlingSubscription<Subscription>( (s) => s.launchStatusChanged?..status..name, onEvent: (s) => debugPrint('${s.launchStatusChanged?.name} changed'), ); return Row(children: [ if (state.isActive) const Icon(CupertinoIcons.dot_radiowaves_left_right), Text(s?.launchStatusChanged?.name ?? 'waiting…'), ]); }}SubscriptionBuilder’s contract (see Subscriptions):
the selector runs once to record the document, the connection opens at the
end of the first frame and closes when the widget is disposed, each event is
normalized into the cache before onEvent runs and the widget rebuilds.
retryAfter: reopens a dropped connection; state.reconnect() does it now.
Testing
Section titled “Testing”The hooks run on the client like the builders do, so
sling_gql_test applies unchanged:
final server = MockGraphQLServer(query: {'latestLaunch': launch181});final client = server.client(Query.root);await tester.pumpWidget( SlingScope<Query>(client: client, schema: slingSchema, child: const LaunchTile()),);await tester.pumpUntilSettled(client);expect(server.requests, hasLength(1));