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).

main.dart — client setup Dart
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.

Fetching a vehicle by ID Dart
// 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:

generated/types/get_vehicle_response.dart Dart
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>:

Fetching all vehicles Dart
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})');
}
generated/types/list_vehicles_response.dart Dart
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:

getPerson — nullable result Dart
// 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:

Adding a vehicle Dart
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:

generated/inputs/add_vehicle_input.dart Dart
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.

Subscribing to new vehicles Dart
// 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:

Error handling Dart
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.

Using _all_fields in a custom query GraphQL
type Query {
  # GraphLink resolves _all_fields to _all_fields_Vehicle for this field
  getVehicle(id: ID!): Vehicle! @glCache(ttl: 120, tags: ["vehicles"])
}