GraphQL in Flutter: Queries, Caching, and Subscriptions with graphql_flutter
A practical guide to GraphQL in Flutter with graphql_flutter: client setup with links and caching, Query and Mutation widgets, optimistic updates, WebSocket subscriptions, hooks, and code generation.
Published on • October 4, 2026
AI Assistant

REST works until you are water-fetching for a UI that wants exactly four fields, then six, then a nested relation. GraphQL in Flutter fixes that negotiation: the client asks for precisely the data it renders, and graphql_flutter wraps that contract in idiomatic widgets, a normalized cache, and real-time subscriptions.
Sources: graphql_flutter on pub.dev (v5.3.0, published by Zino) and the companion graphql client package.
Why graphql_flutter
graphql_flutter is a Flutter wrapper around the pure-Dart graphql client. Current stable is 5.3.0, it is a Flutter Favorite with 900+ likes and 238k+ downloads, and it supports Android, iOS, Linux, macOS, Windows, and web out of the box. Out of the box you get:
- A
GraphQLClientwith a normalized cache (GraphQLCache) that can persist to disk via Hive. - Links — composable middleware for auth, HTTP, WebSockets, retries, and error handling.
- Widgets (
GraphQLProvider,Query,Mutation,Subscription) and matching flutter_hooks APIs (useQuery,useMutation,useSubscription). - Optimistic mutations, pagination with
fetchMore, and file upload support. - Type-safe code generation via
graphql_codegen.
Setting up the client
A GraphQLClient needs two things: a link (how requests travel) and a cache (how responses are stored). Links are concatenated, so auth layers cleanly on top of HTTP:
import 'package:graphql_flutter/graphql_flutter.dart';
Future<ValueNotifier<GraphQLClient>> createClient() async {
await initHiveForFlutter(); // persistent cache on mobile/desktop
final httpLink = HttpLink('https://api.example.com/graphql');
final authLink = AuthLink(
getToken: () async => 'Bearer $accessToken',
);
final link = authLink.concat(httpLink);
return ValueNotifier(GraphQLClient(
link: link,
cache: GraphQLCache(store: HiveStore()),
));
}
initHiveForFlutter() initializes Hive so HiveStore can persist the cache — meaning a cold app start can render previously fetched data instantly. Without it, the default InMemoryStore keeps everything in RAM.
Wrap your app once with the provider:
GraphQLProvider(
client: client,
child: MaterialApp(home: HomeScreen()),
);
Queries: declarative fetching
The Query widget rebuilds itself from the result — no manual stream plumbing:
String readRepos = r"""
query ReadRepositories($n: Int!) {
viewer {
repositories(last: $n) {
nodes { id name viewerHasStarred }
}
}
}
""";
Query(
options: QueryOptions(
document: gql(readRepos),
variables: {'n': 20},
pollInterval: const Duration(seconds: 30),
),
builder: (QueryResult result, {VoidCallback? refetch, FetchMore? fetchMore}) {
if (result.hasException) return Text(result.exception.toString());
if (result.isLoading) return const CircularProgressIndicator();
final nodes = result.data?['viewer']['repositories']['nodes'] as List;
return ListView.builder(
itemCount: nodes.length,
itemBuilder: (_, i) => Text(nodes[i]['name']),
);
},
);
With flutter_hooks the same query collapses into useQuery(QueryOptions(...)), which pairs naturally with Riverpod or Bloc for the rest of your state.
Pagination with fetchMore
Cursor pagination is handled by FetchMoreOptions, where you merge old and new pages yourself:
final opts = FetchMoreOptions(
variables: {'cursor': pageInfo['endCursor']},
updateQuery: (prev, fetchMoreResult) => {
'search': {
'nodes': [
...prev['search']['nodes'],
...fetchMoreResult['search']['nodes'],
],
},
},
);
fetchMore!(opts);
Mutations and optimistic UI
Mutations mirror queries but hand you a RunMutation callback. The interesting part is optimistic results: the cache updates immediately with your predicted state, then reconciles when the server responds.
Mutation(
options: MutationOptions(
document: gql(addStar),
update: (cache, result) => cache,
onCompleted: (data) => print('starred'),
),
builder: (RunMutation runMutation, QueryResult result) {
return IconButton(
icon: const Icon(Icons.star),
onPressed: () => runMutation(
{'starrableId': repoId},
optimisticResult: {
'action': {'starrable': {'viewerHasStarred': true}},
},
),
);
},
);
The cache is broadcast to every watching query, so a starred item flips everywhere in the UI at once — the same illusion of instantaneity you would otherwise hand-roll in your state layer.
Subscriptions: real-time over WebSockets
GraphQL subscriptions run over WebSocket, so you split the link: subscription requests go to WebSocketLink, everything else to HTTP.
final wsLink = WebSocketLink('wss://api.example.com/graphql');
final link = Link.split((request) => request.isSubscription, wsLink, httpLink);
Then consume with the Subscription widget (or useSubscription):
Subscription(
options: SubscriptionOptions(document: gql(r'''
subscription reviewAdded { reviewAdded { stars commentary } }
''')),
builder: (result) {
if (result.hasException) return Text(result.exception.toString());
if (result.isLoading) return const Center(child: CircularProgressIndicator());
return ResultAccumulator.appendUniqueEntries(
latest: result.data,
builder: (context, {results}) => ReviewList(results!.reversed.toList()),
);
},
);
ResultAccumulator collates incoming events for you — handy for chat feeds, dashboards, and collaborative surfaces.
Caching, types, and production notes
- Cache policies. Every query and mutation accepts a
Policy(cacheFirst,networkOnly,cacheAndNetwork,cacheOnly) so you decide when the network is consulted. Direct cache access and fragment writes are available throughGraphQLDataProxy. - Code generation.
graphql_codegenreads.graphqlfiles and emits typed options, variables, and parsed data — turningresult.data?['viewer']into compile-checked structs. - Build requirements. Recent versions require Java 17 and Gradle 8.4 on Android;
graphql_flutternow depends onhive_ce(the community Hive fork),connectivity_plus, andflutter_hooks. - Links are superpowers. Because everything is a
Link, adding retries, logging, persisted queries, or a custom auth refresh is a matter of composing another layer — no framework fork required.
When to use it (and when not to)
Choose GraphQL when your UI consumes overlapping slices of the same entities, you need subscriptions, or your backend already exposes a GraphQL gateway (GitHub, Shopify, GraphCMS, Hasura, AWS AppSync are all supported). Skip it when your API is a simple CRUD endpoint — dio or http with a repository layer will cost you far less ceremony.
Either way, graphql_flutter gives Flutter a mature, cache-aware, real-time-capable API client that plugs straight into the widget tree — one dependency, and your fetching, caching, and live updates share a single declarative contract.
Further reading
- graphql_flutter package docs — widgets, hooks, and setup.
- graphql client README — links, cache strictness, policies, and exceptions.
- graphql_codegen — type-safe operation generation.