Dart / Flutter Client
A fully typed GraphQL client generated directly from your schema.
The adapter pattern
GraphLink is completely agnostic about how HTTP requests are made. The generated GraphLinkClient takes a single function as its HTTP adapter — a Future<String> Function(String payload). This function receives a JSON string (the GraphQL request body) and must return a JSON string (the server response body).
This design lets you use any HTTP client you prefer, without GraphLink importing or depending on any of them. Here are examples with the two most popular Dart HTTP packages:
import 'package:http/http.dart' as http;
Future<String> graphLinkAdapter(String payload) async {
final response = await http.post(
Uri.parse('http://localhost:8080/graphql'),
headers: {'Content-Type': 'application/json'},
body: payload,
);
return response.body;
}
import 'package:dio/dio.dart';
final _dio = Dio();
Future<String> graphLinkAdapter(String payload) async {
final response = await _dio.post<String>(
'http://localhost:8080/graphql',
data: payload,
options: Options(
contentType: 'application/json',
responseType: ResponseType.plain,
),
);
return response.data!;
}
Testing made easy
Because the adapter is just a function, you can pass a mock in tests: (payload) async => '{"data":{"getVehicle":{"id":"1","brand":"Toyota","model":"Camry","year":2023,"fuelType":"GASOLINE","ownerId":null}}}'. No HTTP mocking library needed.
Initializing the client
The GraphLinkClient constructor takes three parameters: the HTTP adapter, an optional WebSocket adapter (needed only for subscriptions), and an optional cache store (uses in-memory by default).
import 'generated/client/graph_link_client.dart';
// HTTP adapter (required)
Future<String> graphLinkAdapter(String payload) async {
final response = await http.post(
Uri.parse('http://localhost:8080/graphql'),
headers: {'Content-Type': 'application/json'},
body: payload,
);
return response.body;
}
// Create the client
// - First arg: HTTP adapter (required)
// - Second arg: WebSocket adapter (optional, for subscriptions)
// - Third arg: Cache store (null = use InMemoryGraphLinkCacheStore)
final client = GraphLinkClient(
graphLinkAdapter,
SimpleWebSocketAdapter('ws://localhost:8080/graphql'),
null,
);
If you do not need subscriptions, pass null for the WebSocket adapter. If you need a custom cache backend (e.g. SharedPreferences on Flutter for persistent caching), pass a GraphLinkCacheStore implementation as the third argument.
Queries
All queries are accessible via client.queries. Each operation becomes a method with typed parameters and a typed return value.
// getVehicle returns GetVehicleResponse — never null (Vehicle! in schema)
final res = await client.queries.getVehicle(id: '42');
// res.getVehicle is a typed Vehicle object
print(res.getVehicle.brand); // Toyota
print(res.getVehicle.model); // Camry
print(res.getVehicle.year); // 2023
print(res.getVehicle.fuelType); // FuelType.GASOLINE
The generated GetVehicleResponse type looks like this:
class GetVehicleResponse {
final Vehicle getVehicle;
GetVehicleResponse({required this.getVehicle});
static GetVehicleResponse fromJson(Map<String, dynamic> json) {
return GetVehicleResponse(
getVehicle: Vehicle.fromJson(json['getVehicle'] as Map<String, dynamic>),
);
}
}
List queries
List queries work the same way. The response type holds a List<Vehicle>:
final res = await client.queries.listVehicles();
// res.listVehicles is List<Vehicle> — fully typed
for (final vehicle in res.listVehicles) {
print('${vehicle.brand} ${vehicle.model} (${vehicle.year})');
}
class ListVehiclesResponse {
final List<Vehicle> listVehicles;
ListVehiclesResponse({required this.listVehicles});
static ListVehiclesResponse fromJson(Map<String, dynamic> json) {
return ListVehiclesResponse(
listVehicles: (json['listVehicles'] as List<dynamic>)
.map((e) => Vehicle.fromJson(e as Map<String, dynamic>)).toList(),
);
}
}
Nullable queries
When the schema declares a query with a nullable return type (no !), the response wrapper field is also nullable. This matches the schema precisely:
// Schema: getPerson(id: ID!): Person <-- no ! on Person
final res = await client.queries.getPerson(id: '99');
// res.getPerson is Person? — use null-aware access
if (res.getPerson != null) {
print(res.getPerson!.name);
}
// Or with null-safe chaining
print(res.getPerson?.email ?? 'Not found');
Mutations
Mutations live under client.mutations and follow the same pattern. Input types are passed as named parameters:
import 'generated/inputs/add_vehicle_input.dart';
import 'generated/enums/fuel_type.dart';
final added = await client.mutations.addVehicle(
input: AddVehicleInput(
brand: 'Toyota',
model: 'Camry',
year: 2023,
fuelType: FuelType.GASOLINE,
// ownerId is nullable — omit it or pass null
),
);
print(added.addVehicle.id); // server-assigned ID
print(added.addVehicle.brand); // Toyota
The generated AddVehicleInput class enforces required fields at construction time through Dart's named required parameters:
class AddVehicleInput {
final String brand;
final String model;
final int year;
final FuelType fuelType;
final String? ownerId;
AddVehicleInput({
required this.brand, required this.model,
required this.year, required this.fuelType, this.ownerId
});
Map<String, dynamic> toJson() {
return { 'brand': brand, 'model': model, 'year': year,
'fuelType': fuelType.toJson(), 'ownerId': ownerId };
}
}
Subscriptions
Subscriptions are available under client.subscriptions and return a Stream. You must provide a WebSocket adapter when constructing the client.
// vehicleAdded() returns Stream<VehicleAddedResponse>
final subscription = client.subscriptions.vehicleAdded().listen((event) {
final vehicle = event.vehicleAdded;
print('New vehicle arrived: ${vehicle.brand} ${vehicle.model}');
print('Year: ${vehicle.year}, Fuel: ${vehicle.fuelType}');
});
// Cancel when done (e.g. when the widget is disposed in Flutter)
subscription.cancel();
The SimpleWebSocketAdapter is a basic implementation bundled with the generated client code. For Flutter, you can use the generated adapter directly. For more advanced scenarios (reconnect logic, auth headers on WS), you can implement the GraphLinkWebSocketAdapter interface yourself.
Error handling
If the server returns a GraphQL error, the generated client throws a GraphLinkException containing the list of errors from the response. Wrap calls in a try/catch:
import 'generated/types/graph_link_error.dart';
try {
final res = await client.queries.getVehicle(id: 'bad-id');
print(res.getVehicle.brand);
} on GraphLinkException catch (e) {
for (final error in e.errors) {
print('GraphQL error: ${error.message}');
if (error.locations != null) {
for (final loc in error.locations!) {
print(' at line ${loc.line}, column ${loc.column}');
}
}
}
} catch (e) {
// Network error, timeout, etc.
print('Request failed: $e');
}
The _all_fields fragment
When generateAllFieldsFragments: true is set in the config, GraphLink generates a named fragment for every type in the schema. For example, for Vehicle it generates a fragment _all_fields_Vehicle that selects every field.
The autoGenerateQueries: true option uses these fragments to automatically build the query strings for every operation. Instead of writing query strings by hand, GraphLink inlines the fields from the fragment. This means you never have to maintain query strings manually — when you add a field to a type in the schema, the generated client automatically fetches that field.
You can also reference _all_fields_Vehicle by name in any hand-written queries you add to the schema. Use the shorthand ... _all_fields and GraphLink resolves it to the appropriate type-specific fragment based on the field's return type.
type Query {
# GraphLink resolves _all_fields to _all_fields_Vehicle for this field
getVehicle(id: ID!): Vehicle! @glCache(ttl: 120, tags: ["vehicles"])
}