<h1 id="using-graphql-in-flutter-apps">Using GraphQL in Flutter Apps</h1> <p>GraphQL offers a flexible and efficient way to fetch and manage data in Flutter applications. This comprehensive guide covers everything from basic setup to advanced features like subscriptions and caching.</p> <h2 id="setting-up-graphql-in-flutter">Setting Up GraphQL in Flutter</h2> <h3 id="package-installation">1. Package Installation</h3> <pre>dependencies: flutter: sdk: flutter graphql_flutter: ^5.0.0 hive: ^2.0.0 hive_flutter: ^1.0.0 </pre> <h3 id="client-configuration">2. Client Configuration</h3> <pre>import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
void main() async { WidgetsFlutterBinding.ensureInitialized(); await Hive.initFlutter(); await Hive.openBox('graphql_cache');
final HttpLink httpLink = HttpLink( 'https://your-graphql-endpoint.com/graphql', defaultHeaders: { 'Authorization': 'Bearer your-token', }, );
final AuthLink authLink = AuthLink( getToken: () async => 'Bearer your-token', );
final Link link = authLink.concat(httpLink);
final ValueNotifier<GraphQLClient> client = ValueNotifier( GraphQLClient( link: link, cache: GraphQLCache(store: HiveStore()), defaultPolicies: DefaultPolicies( query: Policies( fetch: FetchPolicy.networkOnly, error: ErrorPolicy.none, cacheReread: CacheRereadPolicy.mergeOptimistic, ), mutate: Policies( fetch: FetchPolicy.networkOnly, error: ErrorPolicy.none, cacheReread: CacheRereadPolicy.mergeOptimistic, ), subscribe: Policies( fetch: FetchPolicy.networkOnly, error: ErrorPolicy.none, cacheReread: CacheRereadPolicy.mergeOptimistic, ), ), ), );
runApp(MyApp(client: client)); } </pre> <h2 id="writing-queries-and-mutations">Writing Queries and Mutations</h2> <h3 id="query-implementation">1. Query Implementation</h3> <pre>class UserList extends StatelessWidget { const UserList({Key? key}) : super(key: key);
static const String getUsers = r''' query GetUsers($first: Int, $after: String) { users(first: $first, after: $after) { edges { node { id name email posts { id title } } cursor } pageInfo { hasNextPage endCursor } } } ''';
@override Widget build(BuildContext context) { return Query( options: QueryOptions( document: gql(getUsers), variables: { 'first': 10, 'after': null, }, pollInterval: const Duration(seconds: 10), ), builder: (QueryResult result, {VoidCallback? refetch, FetchMore? fetchMore}) { if (result.isLoading) { return const Center(child: CircularProgressIndicator()); }
if (result.hasException) {
return ErrorWidget(result.exception!);
}
final users = result.data?[&#39;users&#39;]?[&#39;edges&#39;] ?? [];
final pageInfo = result.data?[&#39;users&#39;]?[&#39;pageInfo&#39;];
return ListView.builder(
itemCount: users.length + (pageInfo[&#39;hasNextPage&#39;] ? 1 : 0),
itemBuilder: (context, index) {
if (index == users.length) {
return ElevatedButton(
onPressed: () {
fetchMore!(
FetchMoreOptions(
variables: {
&#39;after&#39;: pageInfo[&#39;endCursor&#39;],
},
updateQuery: (previous, next) {
final List&lt;dynamic&gt; repos = [
...previous?[&#39;users&#39;]?[&#39;edges&#39;] ?? [],
...next?[&#39;users&#39;]?[&#39;edges&#39;] ?? [],
];
next?[&#39;users&#39;]?[&#39;edges&#39;] = repos;
return next;
},
),
);
},
child: const Text(&#39;Load More&#39;),
);
}
final user = users[index][&#39;node&#39;];
return ListTile(
title: Text(user[&#39;name&#39;]),
subtitle: Text(user[&#39;email&#39;]),
trailing: Text(&#39;${user[&#39;posts&#39;]?.length ?? 0} posts&#39;),
);
},
);
},
);
} } </pre> <h3 id="mutation-implementation">2. Mutation Implementation</h3> <pre>class AddUserForm extends StatelessWidget { const AddUserForm({Key? key}) : super(key: key);
static const String addUser = r''' mutation AddUser($input: CreateUserInput!) { addUser(input: $input) { id name email posts { id title } } } ''';
@override Widget build(BuildContext context) { return Mutation( options: MutationOptions( document: gql(addUser), update: (GraphQLDataProxy cache, QueryResult? result) { if (result?.data != null) { final newUser = result!.data!['addUser']; final normalizedId = cache.identify(newUser)!; cache.writeQuery( QueryOptions( document: gql(getUsers), ), data: { 'users': { 'edges': [ { 'node': newUser, 'cursor': normalizedId, }, ], }, }, ); } }, onCompleted: (dynamic resultData) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('User added successfully')), ); }, onError: (OperationException? error) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(error.toString())), ); }, ), builder: (RunMutation runMutation, QueryResult? result) { final formKey = GlobalKey<FormState>(); final nameController = TextEditingController(); final emailController = TextEditingController();
return Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: const InputDecoration(labelText: &#39;Name&#39;),
validator: (value) {
if (value?.isEmpty ?? true) {
return &#39;Please enter a name&#39;;
}
return null;
},
),
TextFormField(
controller: emailController,
decoration: const InputDecoration(labelText: &#39;Email&#39;),
validator: (value) {
if (value?.isEmpty ?? true) {
return &#39;Please enter an email&#39;;
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (formKey.currentState?.validate() ?? false) {
runMutation({
&#39;input&#39;: {
&#39;name&#39;: nameController.text,
&#39;email&#39;: emailController.text,
},
});
}
},
child: const Text(&#39;Add User&#39;),
),
],
),
);
},
);
} } </pre> <h2 id="implementing-subscriptions">Implementing Subscriptions</h2> <h3 id="websocket-setup">1. WebSocket Setup</h3> <pre>final WebSocketLink websocketLink = WebSocketLink( url: 'wss://your-graphql-endpoint.com/graphql', config: SocketClientConfig( autoReconnect: true, inactivityTimeout: const Duration(seconds: 30), initialPayload: () async => { 'headers': { 'Authorization': 'Bearer your-token', }, }, ), );
final Link link = Link.split( (request) => request.isSubscription, websocketLink, httpLink, ); </pre> <h3 id="subscription-implementation">2. Subscription Implementation</h3> <pre>class UserUpdates extends StatelessWidget { const UserUpdates({Key? key}) : super(key: key);
static const String userUpdates = r''' subscription OnUserUpdate { userUpdates { id name email status } } ''';
@override Widget build(BuildContext context) { return Subscription( options: SubscriptionOptions( document: gql(userUpdates), ), builder: (QueryResult result) { if (result.isLoading) { return const Center(child: CircularProgressIndicator()); }
if (result.hasException) {
return ErrorWidget(result.exception!);
}
final updates = result.data?[&#39;userUpdates&#39;] ?? [];
return ListView.builder(
itemCount: updates.length,
itemBuilder: (context, index) {
final user = updates[index];
return ListTile(
title: Text(user[&#39;name&#39;]),
subtitle: Text(user[&#39;email&#39;]),
trailing: Text(user[&#39;status&#39;]),
);
},
);
},
);
} } </pre> <h2 id="error-handling-and-retry-logic">Error Handling and Retry Logic</h2> <h3 id="error-handling">1. Error Handling</h3> <pre>class GraphQLErrorHandler { static void handleError(BuildContext context, OperationException? error) { if (error == null) return;
final message = error.graphqlErrors.isNotEmpty
? error.graphqlErrors.first.message
: error.linkException?.toString() ?? &#39;An error occurred&#39;;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
action: SnackBarAction(
label: &#39;Retry&#39;,
onPressed: () {
// Implement retry logic
},
),
),
);
} }
class RetryLink extends Link { final int maxRetries; final Duration retryInterval;
RetryLink({ this.maxRetries = 3, this.retryInterval = const Duration(seconds: 1), });
@override Stream<Response> request(Request request, [NextLink? forward]) async* { int attempts = 0; while (true) { try { yield* forward!(request); break; } catch (e) { attempts++; if (attempts >= maxRetries) rethrow; await Future.delayed(retryInterval); } } } } </pre> <h3 id="cache-management">2. Cache Management</h3> <pre>class CacheManager { static Future<void> clearCache() async { final box = await Hive.openBox('graphql_cache'); await box.clear(); }
static Future<void> updateCache(String query, Map<String, dynamic> data) async { final box = await Hive.openBox('graphql_cache'); await box.put(query, data); }
static Future<Map<String, dynamic>?> getFromCache(String query) async { final box = await Hive.openBox('graphql_cache'); return box.get(query); } } </pre> <h2 id="best-practices">Best Practices</h2> <h3 id="code-organization">1. Code Organization</h3> <ul> <li>Separate queries, mutations, and subscriptions into different files</li> <li>Use fragments for reusable fields</li> <li>Implement proper error handling</li> <li>Use type-safe operations</li> <li>Implement proper caching strategies</li> </ul> <h3 id="performance-optimization">2. Performance Optimization</h3> <ul> <li>Use pagination for large datasets</li> <li>Implement proper caching</li> <li>Use optimistic updates</li> <li>Minimize network requests</li> <li>Implement proper error handling</li> </ul> <h3 id="testing">3. Testing</h3> <ul> <li>Test queries and mutations</li> <li>Test error handling</li> <li>Test caching behavior</li> <li>Test subscription functionality</li> <li>Test performance</li> </ul> <h2 id="conclusion">Conclusion</h2> <p>Implementing GraphQL in Flutter requires:</p> <ul> <li>Proper setup and configuration</li> <li>Understanding of queries, mutations, and subscriptions</li> <li>Implementation of error handling</li> <li>Proper caching strategies</li> <li>Following best practices</li> </ul> <p>Remember to:</p> <ul> <li>Organize your code properly</li> <li>Handle errors gracefully</li> <li>Implement proper caching</li> <li>Test thoroughly</li> <li>Monitor performance</li> </ul> <p>By following these guidelines, you can effectively implement GraphQL in your Flutter applications.</p>