mk_graphql
A clean, modern, simplified GraphQL client for Dart and Flutter with normalized caching, composable links, typed operation execution, file uploads, SSE, WebSockets, automatic pagination, refetch, and granular validation error handling.
π Full documentation β client API, link pipeline, caching internals, and pagination guides.
β¨ Features
- π Composable Link Architecture: Chain
AuthLink,DioLink,SseLink,WebSocketLink,HttpLink,BatchLink, andTypenameLink. - π First-Class Authentication: Automatic 401 handling, thread-safe asynchronous token refreshing, token queueing, and revocation hooks.
- π‘ Multiple Transports:
- HTTP/REST: Built-in
DioLinkand standardHttpLink. - Server-Sent Events (SSE):
SseLinkwith live subscription streaming, dynamic auth headers, and 401 refresh retries. - WebSockets:
WebSocketLinksupporting modern subscriptions.
- HTTP/REST: Built-in
- π Multipart File Uploads: Seamless GraphQL Multipart Request specification compliance for
Upload/MultipartFile. - π Request Cancellation: Direct integration with Dio's
CancelToken. - πΎ Normalized Caching: Fast in-memory cache with canonical JSON key ordering, optimistic responses, and configurable fetch policies (
cacheFirst,networkOnly,cacheAndNetwork,cacheOnly,noCache). - β‘ Stream & Future APIs: Single-shot
client.future()or reactiveclient.stream(). - π Direct Refetching:
client.refetch(req)andreq.refetch(client)with immediate cache synchronization. - π Automatic Pagination (Zero Manual Append):
client.fetchMoreandreq.fetchMoreautomatically merge lists and Relay connections;GraphQLPaginatormanages reactive infinite scroll. - π‘οΈ Validation Errors: Specialized
ValidationExceptionextracting field-level validation messages across Apollo, Laravel/Lighthouse, NestJS, and Spring DGS. - πͺ΅ Built-in Logging: Pretty console logging with custom options and privacy masking.
π¦ Installation
Add mk_graphql to your pubspec.yaml:
dependencies:
mk_graphql: ^1.1.0
Then run:
flutter pub get
π Step-by-Step Integration
1. Initialize GraphQLClient
import 'package:mk_graphql/mk_graphql.dart';
final client = GraphQLClient(
url: 'https://api.example.com/graphql',
// Optional: SSE or WebSocket URL for subscriptions
sseUrl: 'https://api.example.com/graphql/sse',
// wsUrl: 'wss://api.example.com/graphql',
// Attach dynamic authentication token to every outgoing request
tokenHeader: () async {
final token = await getStoredToken();
return token != null ? {'Authorization': 'Bearer $token'} : {};
},
// Called automatically on 401 responses to refresh token and retry
onTokenRefresh: (dioLink) async {
final newToken = await refreshUserToken();
return newToken != null ? {'Authorization': 'Bearer $newToken'} : null;
},
// Default caching policy: cacheFirst, networkOnly, cacheAndNetwork, cacheOnly, noCache
defaultFetchPolicy: FetchPolicy.cacheFirst,
// Default error policy: none (throw if errors), ignore (return partial data), all (return data with errors)
defaultErrorPolicy: ErrorPolicy.none,
);
2. Execute Queries & Mutations
client.future(req) executes the operation once, handles caching according to fetchPolicy, and returns typed TData directly (or throws a typed GraphQLException):
try {
// Using a generated or custom GraphQLRequest
final data = await client.future(getUserReq);
print('User: ${data.user.name}');
} on ValidationException catch (e) {
// Field-specific validation failures
print('Field errors: ${e.validationErrors}');
if (e.hasError('email')) {
print('Email error: ${e.getFirstError('email')}');
}
} on UnAuthorizedException {
// Session expired or unauthenticated
navigateToLogin();
} on GraphQLException catch (e) {
// Network, server, or execution error
print('GraphQL Error: ${e.message}');
}
Or execute directly on the request object:
final data = await getUserReq.execute(client);
3. Reactive Streams & Cache Watching
Watch cache updates reactively:
// Stream network emissions according to FetchPolicy (e.g. cacheAndNetwork emits cache then network)
final subscription = client.stream(getUserReq).listen((data) {
print('Received data: ${data.user.name}');
});
// Or watch normalized cache directly whenever any operation updates it
client.watchQuery(getUserReq).listen((cachedData) {
print('Cache updated: ${cachedData?.user.name}');
});
4. Refetching Queries
Bypass cache with FetchPolicy.networkOnly, retrieve fresh network data, and update the cache in-place:
// Using client:
final freshData = await client.refetch(getUserReq);
// Using request extension:
final freshData2 = await getUserReq.refetch(client);
5. Automatic Pagination (Zero Manual Append Required)
You do not need to manually concatenate lists ([...previous, ...incoming]).
Option A: client.fetchMore / req.fetchMore
Automatically merges response JSON trees (concatenating list fields and merging Relay connections) and updates the cache under the initial request:
// Fetch next page and merge automatically:
final mergedData = await client.fetchMore(
originalRequest: page1Req,
nextRequest: page2Req,
);
// Or via request extension:
final mergedData2 = await page1Req.fetchMore(client, nextRequest: page2Req);
// Active listeners on page1Req automatically emit the accumulated data!
Option B: GraphQLPaginator
For infinite scroll lists with reactive state management:
// 1. Create a paginator (page-number, offset/limit, or Relay cursor):
final paginator = client.paginatePage(
initialRequest: GetAnimePageReq(
variables: const GetAnimePageVars(page: 1, perPage: 20),
),
getItems: (data) => data.page?.media ?? [],
updatePage: (vars, nextPage) => vars.copyWith(page: nextPage),
pageSize: 20,
getPage: (data) => data.page?.pageInfo?.currentPage,
hasNextPage: (data) => data.page?.pageInfo?.hasNextPage ?? false,
);
// 2. Load initial page (returns List<TItem> directly)
final firstPageItems = await paginator.loadInitial();
// 3. Load next page (automatically accumulates and returns complete list)
final accumulatedItems = await paginator.loadNext();
// 4. Dynamically apply search queries or filters (restarts page 1 without wiping other criteria):
paginator.applyVariables((currentVars) => currentVars.copyWith(
search: 'Naruto',
genre: 'Action',
));
// 5. Pull-to-refresh (resets query back to initialRequest):
await paginator.refetch();
// 6. Reactive State Stream:
paginator.stream.listen((state) {
print('Items: ${state.items.length}, LoadingMore: ${state.isLoadingMore}');
});
Flutter BLoC Integration (emit.forEach):
Pipe paginator.stream directly into BLoC state without manual subscriptions:
class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
final GraphQLPaginator _paginator;
AnimeListBloc(this._paginator) : super(const AnimeListState()) {
on<AnimeListStarted>((event, emit) async {
await emit.forEach<PaginationState<MediaPartialModel>>(
_paginator.stream,
onData: (pState) => state.copyWith(
items: pState.items,
isLoading: pState.isLoading,
isLoadingMore: pState.isLoadingMore,
hasMore: pState.hasMore,
error: pState.error,
),
);
});
on<SearchChanged>((event, emit) {
_paginator.applyVariables((v) => v.copyWith(search: event.query));
});
on<FetchNextPage>((event, emit) => _paginator.loadNext());
on<Refreshed>((event, emit) => _paginator.refetch());
}
}
6. Subscriptions (SSE or WebSockets)
final client = GraphQLClient(
url: 'https://api.example.com/graphql',
sseUrl: 'https://api.example.com/graphql/sse',
tokenHeader: () async => {'Authorization': 'Bearer $token'},
);
client.stream(mySubscriptionRequest).listen((data) {
print('Real-time event: $data');
});
7. Multipart File Uploads
Conforms to the standard GraphQL Multipart Request specification:
import 'package:dio/dio.dart';
import 'package:mk_graphql/mk_graphql.dart';
final file = MultipartFile.fromFileSync(
'/path/to/avatar.png',
filename: 'avatar.png',
);
final uploadReq = UploadAvatarReq(
variables: UploadAvatarVars(file: file),
);
final result = await client.future(uploadReq);
8. Request Cancellation with CancelToken
final cancelToken = CancelToken();
// Cancel request when user navigates away
void onDispose() {
cancelToken.cancel('User navigated away');
}
try {
final data = await client.future(myRequest, cancelToken: cancelToken);
} on RequestCancelledException catch (e) {
print('Request cancelled: ${e.message}');
}
π Full Documentation
- Client, Requests & ErrorPolicy
- Normalized Cache & Optimistic UI
- Link Pipeline (Auth, SSE, WS, Uploads)
- Pagination Guide
- Code Generator (
mk_graphql_generator)
π License
This project is licensed under the MIT License - see the LICENSE file for details.