Using GraphQL in Flutter Apps
•18 min read
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.
Setting Up GraphQL in Flutter
1. Package Installation
dependencies: flutter: sdk: flutter graphql_flutter: ^5.0.0 hive: ^2.0.0 hive_flutter: ^1.0.0
2. Client Configuration
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)); }
Writing Queries and Mutations
1. Query Implementation
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?['users']?['edges'] ?? []; final pageInfo = result.data?['users']?['pageInfo']; return ListView.builder( itemCount: users.length + (pageInfo['hasNextPage'] ? 1 : 0), itemBuilder: (context, index) { if (index == users.length) { return ElevatedButton( onPressed: () { fetchMore!( FetchMoreOptions( variables: { 'after': pageInfo['endCursor'], }, updateQuery: (previous, next) { final List<dynamic> repos = [ ...previous?['users']?['edges'] ?? [], ...next?['users']?['edges'] ?? [], ]; next?['users']?['edges'] = repos; return next; }, ), ); }, child: const Text('Load More'), ); } final user = users[index]['node']; return ListTile( title: Text(user['name']), subtitle: Text(user['email']), trailing: Text('${user['posts']?.length ?? 0} posts'), ); }, ); }, ); } }
2. Mutation Implementation
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: 'Name'), validator: (value) { if (value?.isEmpty ?? true) { return 'Please enter a name'; } return null; }, ), TextFormField( controller: emailController, decoration: const InputDecoration(labelText: 'Email'), validator: (value) { if (value?.isEmpty ?? true) { return 'Please enter an email'; } return null; }, ), ElevatedButton( onPressed: () { if (formKey.currentState?.validate() ?? false) { runMutation({ 'input': { 'name': nameController.text, 'email': emailController.text, }, }); } }, child: const Text('Add User'), ), ], ), ); }, ); } }
Implementing Subscriptions
1. WebSocket Setup
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, );
2. Subscription Implementation
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?['userUpdates'] ?? []; return ListView.builder( itemCount: updates.length, itemBuilder: (context, index) { final user = updates[index]; return ListTile( title: Text(user['name']), subtitle: Text(user['email']), trailing: Text(user['status']), ); }, ); }, ); } }
Error Handling and Retry Logic
1. Error Handling
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() ?? 'An error occurred'; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), action: SnackBarAction( label: 'Retry', 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); } } } }
2. Cache Management
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); } }
Best Practices
1. Code Organization
- Separate queries, mutations, and subscriptions into different files
- Use fragments for reusable fields
- Implement proper error handling
- Use type-safe operations
- Implement proper caching strategies
2. Performance Optimization
- Use pagination for large datasets
- Implement proper caching
- Use optimistic updates
- Minimize network requests
- Implement proper error handling
3. Testing
- Test queries and mutations
- Test error handling
- Test caching behavior
- Test subscription functionality
- Test performance
Conclusion
Implementing GraphQL in Flutter requires:
- Proper setup and configuration
- Understanding of queries, mutations, and subscriptions
- Implementation of error handling
- Proper caching strategies
- Following best practices
Remember to:
- Organize your code properly
- Handle errors gracefully
- Implement proper caching
- Test thoroughly
- Monitor performance
By following these guidelines, you can effectively implement GraphQL in your Flutter applications.