Typsicher & elegant: So integrierst du GraphQL in Flutter

Wir bauen gern schöne Dinge. Und wir bauen sie gern richtig. Als wir eine Flutter-App mit einer GraphQL-API verbinden wollten, war für uns klar: Das muss nicht nur funktionieren – es soll auch Spass machen. Entwickler*innen sollen Autocompletion, Typensicherheit und direkte Verifizierung gegen das API-Schema bekommen. Was wir dafür verwendet haben? graphql_flutter, graphql_codegen und ein bisschen Magie.
Warum überhaupt GraphQL?
Weil wir nur holen wollen, was wir brauchen – nicht mehr, nicht weniger. GraphQL erlaubt genau das: fein granulierte Datenabfragen über eine einzige Schnittstelle.
Falls du grad nicht mehr ganz sicher bist, was GraphQL genau ist:
👉 Hier gibt’s eine einfache Erklärung
Struktur matters: So sieht unser Projekt aus
Ordnung ist das halbe Leben – und in der Codebasis die ganze Miete. Damit unsere Flutter-App auch langfristig wartbar und übersichtlich bleibt, haben wir unsere lib/data/graphql
-Struktur klar gegliedert:
lib/
└── data/
└── graphql/
├── __generated/ # Hier landen alle generierten dart-Dateien
├── fragments/ # Wiederverwendbare Snippets (.graphql)
├── mutations/ # GraphQL-Mutationen (.graphql)
├── queries/ # GraphQL-Queries (.graphql)
├── schema/
│ └── schema.graphql # Das API-Schema (via Symlink eingebunden)
├── subscriptions/ # Subscriptions, falls benötigt (.graphql)
├── graphql_service.dart # Setzt den GraphQLClient auf
└── index.dart. # Barrelfile für einfache Exporte
Fragments, Mutations, Queries und Subscriptions schreiben wir direkt als .graphql
-Dateien – sauber getrennt in die jeweiligen Ordner. Das sorgt für Übersicht und eine klare Trennung zwischen Datenlogik und Flutter-Code.
Das zentrale Schema stammt in der Regel vom Backend und lebt ausserhalb der Flutter-App. Damit wir in Flutter trotzdem damit arbeiten können, binden wir es per Symlink ins Projekt ein. So profitieren wir direkt von:
- 🧠 Autocompletion beim Schreiben der
.graphql
-Dateien - 🛡️ Schema-Verifikation direkt im Editor
- 🔄 jederzeit aktueller Stand – weil wir auf dasselbe Schema zugreifen wie das Backend-Team.
So setzt du den nötigen Symlink:
cd lib/data/graphql/schema
ln -s <relative-path-to-schema.graphql> ./schema.graphql
Alternativ kann das Schema auch direkt ins Projekt kopiert werden – weniger elegant, aber funktioniert genauso.
Wichtig:
Der Ordner __generated/
gehört nicht in die Versionskontrolle (Git). Deshalb ist er bei uns konsequent im .gitignore
eingetragen – der Inhalt wird bei jedem Build-Prozess frisch erzeugt.
# .gitignore
lib/data/graphql/__generated/
Barrelfiles für simple Imports (optional)
Damit wir nicht jede Datei einzeln importieren müssen, setzen wir in jedem relevanten Ordner auf sogenannte Barrelfiles (z.B. index.dart
). Diese exportieren alle wichtigen Inhalte eines Ordners:
// lib/data/graphql/queries/index.dart
export 'my-query-1.graphql.dart';
export 'my-query-2.graphql.dart';
...
Dadurch können wir den gesamten GraphQL-Layer zentral importieren – z.B. so:
import 'package:myproject/data/graphql/index.dart';
Das reduziert Boilerplate und hält unsere Flutter-Komponenten sauber und kompakt.
Die index.dart
-Files müssen auch nicht in jedem Ordner manuell erstellt werden – Barrelfiles lassen sich nämlich ganz easy automatisieren 💡
Ich habe mir dafür von ChatGPT ein kleines Dart-Script schreiben lassen, das ich jeweils direkt nach dem build_runner
durchlaufen lasse – so sind auch die Barrelfiles immer aktuell. Das Script entfernt zuerst alle alten index.dart
-Dateien und generiert dann neue – inklusive Subdirectory-Handling:
import 'dart:io';
void main() {
final rootDir = Directory('lib/data/graphql');
if (!rootDir.existsSync()) {
print('Das Verzeichnis ${rootDir.path} existiert nicht.');
return;
}
print('Alte Barrel Files entfernen...');
deleteExistingBarrelFiles(rootDir);
print('Generiere neue Barrel Files für ${rootDir.path}...');
generateBarrelFiles(rootDir);
}
/// Löscht alle existierenden index.dart-Dateien im Verzeichnis und dessen Unterverzeichnissen
void deleteExistingBarrelFiles(Directory dir) {
for (var entity in dir.listSync(recursive: true)) {
if (entity is File && entity.path.endsWith('index.dart')) {
entity.deleteSync();
print('Gelöscht: ${entity.path}');
}
}
}
/// Generiert Barrel Files für ein Verzeichnis und seine Unterverzeichnisse
bool generateBarrelFiles(Directory dir) {
final dartFiles = <File>[];
final subDirs = <Directory>[];
final generatedSubDirIndexes = <Directory>[];
// Sammle alle .dart-Dateien und Unterverzeichnisse
for (var entity in dir.listSync(recursive: false)) {
if (entity is File &&
entity.path.endsWith('.dart') &&
!entity.path.endsWith('index.dart')) {
dartFiles.add(entity);
} else if (entity is Directory) {
subDirs.add(entity);
}
}
// Rekursiv für Unterverzeichnisse weitermachen und prüfen, ob index.dart erstellt wurde
for (var subDir in subDirs) {
if (generateBarrelFiles(subDir)) {
generatedSubDirIndexes.add(subDir);
}
}
// Barrel File für das aktuelle Verzeichnis erstellen
if (dartFiles.isNotEmpty || generatedSubDirIndexes.isNotEmpty) {
final barrelFile = File('${dir.path}/index.dart');
final buffer = StringBuffer();
// .dart-Dateien exportieren
for (var file in dartFiles) {
final filename = file.uri.pathSegments.last;
buffer.writeln("export '$filename';");
}
// Nur Unterverzeichnisse exportieren, deren index.dart erstellt wurde
for (var subDir in generatedSubDirIndexes) {
final subDirName =
subDir.uri.pathSegments[subDir.uri.pathSegments.length - 2];
buffer.writeln("export '$subDirName/index.dart';");
}
barrelFile.writeAsStringSync(buffer.toString());
print('Barrel File erstellt: ${barrelFile.path}');
return true;
}
return false;
}
Einfach als tools/generate_graphql_barrels.dart
abspeichern und nach dem Codegen ausführen – z. B. mit:
dart tools/generate_graphql_barrels.dart
So bleibt alles sauber und up to date – ganz ohne manuelles Hin- und Her.
Developer Experience: Smooth dank Tooling
Damit die Integration von GraphQL in Flutter nicht nur funktioniert, sondern auch Spass macht, setzen wir auf ein Setup, das möglichst viel für uns übernimmt – von Typensicherheit bis Autocompletion.
Das Setup in vier einfachen Schritten:
1️⃣ Packages installieren
Zuerst installieren wir die nötigen Packages:
flutter pub add graphql_flutter
flutter pub add --dev graphql_codegen build_runner
graphql_flutter
stellt den GraphQL-Client zur Verfügung, mit allem Drum und Dran.graphql_codegen
undbuild_runner
sorgen für die automatische Generierung von Dart-Code aus unseren.graphql
-Dateien.
2️⃣ build.yaml konfigurieren
Im build.yaml
definieren wir, wo unsere GraphQL-Dateien liegen und wohin der generierte Code geschrieben werden soll. Ausserdem legen wir fest, dass wir graphql_flutter
als Client verwenden – damit queries und mutators automatisch im richtigen Format erzeugt werden.
# build.yaml
targets:
$default:
builders:
graphql_codegen:
options:
clients:
- graphql_flutter
outputDirectory: /lib/data/graphql/__generated
assetsPath: lib/data/graphql/**.graphql
3️⃣ VS Code Extension installieren
👉 GraphQL: Language Feature Support
Die Extension bringt uns Autocompletion, Syntax-Highlighting und Schema-Verifikation direkt im Editor. Ein Gamechanger, versprochen 😎
4️⃣ graphql.config.yaml anlegen
Um das Ganze miteinander zu verbinden, braucht’s noch eine Konfigurationsdatei mit Verweis auf das Schema und unsere Queries:
# graphql.config.yml
schema: 'lib/data/graphql/schema/schema.graphql'
documents: 'lib/**/*.{graphql,dart}'
Unser GraphQLService: Eine zentrale Schnittstelle zum Backend
Damit unsere App effizient mit der GraphQL-API kommuniziert, setzen wir auf einen zentralen GraphQLService
, der die gesamte Kommunikation kapselt. So haben wir volle Kontrolle über Authentifizierung, Fehlerhandling, Logging, etc. – und können überall im Projekt auf einen vorkonfigurierten GraphQLClient
zugreifen.
class GraphQLService {
final _authenticationService = locator<AuthenticationService>();
late GraphQLClient client;
GraphQLService() {
// Basis-HTTP-Link mit API-URL – zielt auf dein GraphQL-Backend
final httpLink = HttpLink(
AppEnvironment.apiBaseUrl,
);
// Authentifizierung – hier holen wir das Access Token aus unserem AuthService
// Du kannst hier auch eine andere Logik einbauen (z. B. Refresh Token, Whitelist etc.)
final authLink = AuthLink(
getToken: () async {
final accessToken = await _authenticationService.getAccessToken();
return accessToken != null ? 'Bearer $accessToken' : null;
},
);
// Die Link-Kette – hier kannst du auch Logging, Retry oder andere Middleware einhängen
final link = authLink.concat(httpLink);
client = GraphQLClient(
link: link,
// Cache-Strategie – hier verwenden wir einen InMemoryStore
// Für persistentes Caching könntest du z. B. HiveStore einsetzen
cache: GraphQLCache(store: InMemoryStore()),
// Timeout für Queries und Mutations – kann je nach Use Case angepasst werden
queryRequestTimeout: const Duration(seconds: 10));
// Du kannst dir hier zusätzliche Hilfsmethoden bauen, z. B. für typisierte Calls
// oder zentrale Fehlerbehandlung
}
🎉 Have fun: Typisierte API-Calls – überall im Projekt
Ob im Bloc, ViewModel oder einem anderen Service: Dank graphql_codegen
und unserem zentralen GraphQLService
greifen wir überall in unserer Codebase typisiert auf unsere API zu.
final result = await _graphQLService.client.query$MyQuery1();
Kein String-Gefrickel mehr, keine Tippfehler, keine vergessenen Felder – stattdessen volle Autocompletion, Typensicherheit und ein Setup, das sich einfach gut anfühlt.
Falls du selbst mit Flutter und GraphQL loslegen willst – do it!
Und wenn du Fragen hast oder nicht weiterkommst: Wir helfen gerne 💜

Geschrieben von
Nando Schär