GraphQL
Complete examples of using Fasq with GraphQL APIs. Learn how to integrate GraphQL queries, mutations, and subscriptions with Fasq’s caching and state management.
Basic GraphQL Setup
Section titled “Basic GraphQL Setup”GraphQL Client Configuration
Section titled “GraphQL Client Configuration”import 'package:graphql_flutter/graphql_flutter.dart';import 'package:fasq/fasq.dart';
class GraphQLService { static final HttpLink _httpLink = HttpLink('https://api.example.com/graphql'); static final AuthLink _authLink = AuthLink( getToken: () async => 'Bearer ${await getToken()}', ); static final Link _link = _authLink.concat(_httpLink);
static final GraphQLClient _client = GraphQLClient( link: _link, cache: GraphQLCache(store: InMemoryStore()), );
static GraphQLClient get client => _client;}
// GraphQL queriesconst String getUsersQuery = ''' query GetUsers { users { id name email posts { id title content } } }''';
const String getUserQuery = ''' query GetUser(\$id: ID!) { user(id: \$id) { id name email posts { id title content } } }''';
const String createUserMutation = ''' mutation CreateUser(\$input: CreateUserInput!) { createUser(input: \$input) { id name email } }''';
const String updateUserMutation = ''' mutation UpdateUser(\$id: ID!, \$input: UpdateUserInput!) { updateUser(id: \$id, input: \$input) { id name email } }''';
const String deleteUserMutation = ''' mutation DeleteUser(\$id: ID!) { deleteUser(id: \$id) }''';GraphQL Queries
Section titled “GraphQL Queries”Basic GraphQL Query
Section titled “Basic GraphQL Query”class GraphQLUsersScreen extends StatelessWidget { @override Widget build(BuildContext context) { return QueryBuilder<List<User>>( queryKey: 'graphql-users', queryFn: () => _fetchUsers(), builder: (context, state) { return state.when( loading: () => Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), data: (users) => ListView.builder( itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), trailing: Text('${user.posts.length} posts'), ); }, ), ); }, ); }
Future<List<User>> _fetchUsers() async { final result = await GraphQLService.client.query( QueryOptions( document: gql(getUsersQuery), ), );
if (result.hasException) { throw Exception(result.exception.toString()); }
final usersData = result.data?['users'] as List<dynamic>?; return usersData?.map((json) => User.fromJson(json)).toList() ?? []; }}Parameterized GraphQL Query
Section titled “Parameterized GraphQL Query”class GraphQLUserDetailScreen extends StatelessWidget { final String userId;
const GraphQLUserDetailScreen({required this.userId});
@override Widget build(BuildContext context) { return QueryBuilder<User>( queryKey: 'graphql-user:$userId', queryFn: () => _fetchUser(userId), builder: (context, state) { return state.when( loading: () => Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), data: (user) => Column( children: [ Text('Name: ${user.name}'), Text('Email: ${user.email}'), Text('Posts: ${user.posts.length}'), Expanded( child: ListView.builder( itemCount: user.posts.length, itemBuilder: (context, index) { final post = user.posts[index]; return ListTile( title: Text(post.title), subtitle: Text(post.content), ); }, ), ), ], ), ); }, ); }
Future<User> _fetchUser(String userId) async { final result = await GraphQLService.client.query( QueryOptions( document: gql(getUserQuery), variables: {'id': userId}, ), );
if (result.hasException) { throw Exception(result.exception.toString()); }
final userData = result.data?['user']; if (userData == null) { throw Exception('User not found'); }
return User.fromJson(userData); }}GraphQL Mutations
Section titled “GraphQL Mutations”Create User Mutation
Section titled “Create User Mutation”class GraphQLCreateUserScreen extends StatefulWidget { @override State<GraphQLCreateUserScreen> createState() => _GraphQLCreateUserScreenState();}
class _GraphQLCreateUserScreenState extends State<GraphQLCreateUserScreen> { final _nameController = TextEditingController(); final _emailController = TextEditingController();
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Create User (GraphQL)')), body: MutationBuilder<User, Map<String, String>>( mutationFn: (data) => _createUser(data), options: MutationOptions( onSuccess: (user) { // Invalidate users query to refetch QueryClient().invalidateQuery('graphql-users'); Navigator.pop(context); }, ), builder: (context, state) { return Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ TextField( controller: _nameController, decoration: InputDecoration(labelText: 'Name'), ), SizedBox(height: 16), TextField( controller: _emailController, decoration: InputDecoration(labelText: 'Email'), ), SizedBox(height: 24), ElevatedButton( onPressed: state.isLoading ? null : () { if (_nameController.text.isNotEmpty && _emailController.text.isNotEmpty) { state.mutate({ 'name': _nameController.text, 'email': _emailController.text, }); } }, child: state.isLoading ? CircularProgressIndicator() : Text('Create User'), ), if (state.hasError) Text('Error: ${state.error}'), if (state.hasData) Text('Created: ${state.data!.name}'), ], ), ); }, ), ); }
Future<User> _createUser(Map<String, String> data) async { final result = await GraphQLService.client.mutate( MutationOptions( document: gql(createUserMutation), variables: { 'input': { 'name': data['name'], 'email': data['email'], }, }, ), );
if (result.hasException) { throw Exception(result.exception.toString()); }
final userData = result.data?['createUser']; if (userData == null) { throw Exception('Failed to create user'); }
return User.fromJson(userData); }}Update User Mutation
Section titled “Update User Mutation”class GraphQLUpdateUserScreen extends StatefulWidget { final User user;
const GraphQLUpdateUserScreen({required this.user});
@override State<GraphQLUpdateUserScreen> createState() => _GraphQLUpdateUserScreenState();}
class _GraphQLUpdateUserScreenState extends State<GraphQLUpdateUserScreen> { late final TextEditingController _nameController; late final TextEditingController _emailController;
@override void initState() { super.initState(); _nameController = TextEditingController(text: widget.user.name); _emailController = TextEditingController(text: widget.user.email); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Update User (GraphQL)')), body: MutationBuilder<User, Map<String, String>>( mutationFn: (data) => _updateUser(data), options: MutationOptions( onSuccess: (user) { // Invalidate related queries QueryClient().invalidateQuery('graphql-users'); QueryClient().invalidateQuery('graphql-user:${widget.user.id}'); Navigator.pop(context); }, ), builder: (context, state) { return Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ TextField( controller: _nameController, decoration: InputDecoration(labelText: 'Name'), ), SizedBox(height: 16), TextField( controller: _emailController, decoration: InputDecoration(labelText: 'Email'), ), SizedBox(height: 24), ElevatedButton( onPressed: state.isLoading ? null : () { state.mutate({ 'name': _nameController.text, 'email': _emailController.text, }); }, child: state.isLoading ? CircularProgressIndicator() : Text('Update User'), ), if (state.hasError) Text('Error: ${state.error}'), if (state.hasData) Text('Updated: ${state.data!.name}'), ], ), ); }, ), ); }
Future<User> _updateUser(Map<String, String> data) async { final result = await GraphQLService.client.mutate( MutationOptions( document: gql(updateUserMutation), variables: { 'id': widget.user.id, 'input': { 'name': data['name'], 'email': data['email'], }, }, ), );
if (result.hasException) { throw Exception(result.exception.toString()); }
final userData = result.data?['updateUser']; if (userData == null) { throw Exception('Failed to update user'); }
return User.fromJson(userData); }}GraphQL Subscriptions
Section titled “GraphQL Subscriptions”Real-time Updates
Section titled “Real-time Updates”class GraphQLSubscriptionExample extends StatefulWidget { @override State<GraphQLSubscriptionExample> createState() => _GraphQLSubscriptionExampleState();}
class _GraphQLSubscriptionExampleState extends State<GraphQLSubscriptionExample> { late StreamSubscription _subscription; List<User> _users = [];
@override void initState() { super.initState(); _setupSubscription(); }
void _setupSubscription() { _subscription = GraphQLService.client.subscribe( SubscriptionOptions( document: gql(''' subscription UserUpdates { userUpdates { type user { id name email } } } '''), ), ).listen((result) { if (result.hasException) { print('Subscription error: ${result.exception}'); return; }
final data = result.data?['userUpdates']; if (data != null) { final type = data['type'] as String; final userData = data['user'] as Map<String, dynamic>; final user = User.fromJson(userData);
setState(() { switch (type) { case 'CREATED': _users.add(user); break; case 'UPDATED': final index = _users.indexWhere((u) => u.id == user.id); if (index != -1) { _users[index] = user; } break; case 'DELETED': _users.removeWhere((u) => u.id == user.id); break; } });
// Invalidate cache to sync with server QueryClient().invalidateQuery('graphql-users'); } }); }
@override void dispose() { _subscription.cancel(); super.dispose(); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Real-time Users (GraphQL)')), body: ListView.builder( itemCount: _users.length, itemBuilder: (context, index) { final user = _users[index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), ); }, ), ); }}Advanced GraphQL Patterns
Section titled “Advanced GraphQL Patterns”Fragment Usage
Section titled “Fragment Usage”const String userFragment = ''' fragment UserFragment on User { id name email createdAt updatedAt }''';
const String postFragment = ''' fragment PostFragment on Post { id title content author { ...UserFragment } }''';
const String getPostsWithUsersQuery = ''' query GetPostsWithUsers { posts { ...PostFragment } } $postFragment $userFragment''';
class GraphQLPostsWithUsersScreen extends StatelessWidget { @override Widget build(BuildContext context) { return QueryBuilder<List<Post>>( queryKey: 'graphql-posts-with-users', queryFn: () => _fetchPostsWithUsers(), builder: (context, state) { return state.when( loading: () => Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), data: (posts) => ListView.builder( itemCount: posts.length, itemBuilder: (context, index) { final post = posts[index]; return Card( child: ListTile( title: Text(post.title), subtitle: Text(post.content), trailing: Text('By: ${post.author.name}'), ), ); }, ), ); }, ); }
Future<List<Post>> _fetchPostsWithUsers() async { final result = await GraphQLService.client.query( QueryOptions( document: gql(getPostsWithUsersQuery), ), );
if (result.hasException) { throw Exception(result.exception.toString()); }
final postsData = result.data?['posts'] as List<dynamic>?; return postsData?.map((json) => Post.fromJson(json)).toList() ?? []; }}Batch Operations
Section titled “Batch Operations”class GraphQLBatchOperationsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Batch Operations (GraphQL)')), body: Column( children: [ ElevatedButton( onPressed: () => _batchCreateUsers(), child: Text('Batch Create Users'), ), ElevatedButton( onPressed: () => _batchUpdateUsers(), child: Text('Batch Update Users'), ), ElevatedButton( onPressed: () => _batchDeleteUsers(), child: Text('Batch Delete Users'), ), ], ), ); }
Future<void> _batchCreateUsers() async { final users = [ {'name': 'User 1', 'email': 'user1@example.com'}, {'name': 'User 2', 'email': 'user2@example.com'}, {'name': 'User 3', 'email': 'user3@example.com'}, ];
final futures = users.map((userData) => GraphQLService.client.mutate( MutationOptions( document: gql(createUserMutation), variables: {'input': userData}, ), ), );
try { await Future.wait(futures); QueryClient().invalidateQuery('graphql-users'); print('Batch create completed'); } catch (error) { print('Batch create failed: $error'); } }
Future<void> _batchUpdateUsers() async { final updates = [ {'id': '1', 'name': 'Updated User 1'}, {'id': '2', 'name': 'Updated User 2'}, {'id': '3', 'name': 'Updated User 3'}, ];
final futures = updates.map((updateData) => GraphQLService.client.mutate( MutationOptions( document: gql(updateUserMutation), variables: { 'id': updateData['id'], 'input': {'name': updateData['name']}, }, ), ), );
try { await Future.wait(futures); QueryClient().invalidateQuery('graphql-users'); print('Batch update completed'); } catch (error) { print('Batch update failed: $error'); } }
Future<void> _batchDeleteUsers() async { final userIds = ['1', '2', '3'];
final futures = userIds.map((userId) => GraphQLService.client.mutate( MutationOptions( document: gql(deleteUserMutation), variables: {'id': userId}, ), ), );
try { await Future.wait(futures); QueryClient().invalidateQuery('graphql-users'); print('Batch delete completed'); } catch (error) { print('Batch delete failed: $error'); } }}Error Handling
Section titled “Error Handling”GraphQL Error Handling
Section titled “GraphQL Error Handling”class GraphQLErrorHandlingExample extends StatelessWidget { @override Widget build(BuildContext context) { return QueryBuilder<List<User>>( queryKey: 'graphql-users-with-error-handling', queryFn: () => _fetchUsersWithErrorHandling(), builder: (context, state) { if (state.hasError) { return _buildErrorWidget(state.error!); }
return state.when( loading: () => Center(child: CircularProgressIndicator()), data: (users) => ListView.builder( itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return ListTile(title: Text(user.name)); }, ), ); }, ); }
Future<List<User>> _fetchUsersWithErrorHandling() async { try { final result = await GraphQLService.client.query( QueryOptions( document: gql(getUsersQuery), ), );
if (result.hasException) { final exception = result.exception;
if (exception is OperationException) { // Handle GraphQL errors final errors = exception.graphqlErrors; if (errors.isNotEmpty) { throw GraphQLError(errors.first.message); } }
throw Exception('GraphQL operation failed'); }
final usersData = result.data?['users'] as List<dynamic>?; return usersData?.map((json) => User.fromJson(json)).toList() ?? []; } catch (error) { if (error is GraphQLError) { rethrow; }
// Handle network errors throw NetworkError('Failed to fetch users: $error'); } }
Widget _buildErrorWidget(Object error) { if (error is GraphQLError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error, size: 64, color: Colors.red), SizedBox(height: 16), Text('GraphQL Error'), SizedBox(height: 8), Text(error.message), ], ), ); } else if (error is NetworkError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.wifi_off, size: 64, color: Colors.orange), SizedBox(height: 16), Text('Network Error'), SizedBox(height: 8), Text(error.message), ], ), ); } else { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error, size: 64, color: Colors.red), SizedBox(height: 16), Text('Error'), SizedBox(height: 8), Text('$error'), ], ), ); } }}
class GraphQLError implements Exception { final String message; GraphQLError(this.message);}
class NetworkError implements Exception { final String message; NetworkError(this.message);}Performance Tips
Section titled “Performance Tips”- Use fragments - Reuse common field selections
- Batch operations - Combine multiple operations when possible
- Implement subscriptions - Use real-time updates for live data
- Handle errors gracefully - Provide specific error messages
- Cache GraphQL results - Leverage Fasq’s caching for GraphQL data
- Optimize queries - Only request needed fields
- Use variables - Parameterize queries for reusability
Next Steps
Section titled “Next Steps”- Authentication - Learn about authentication patterns
- CRUD Operations - Learn about CRUD patterns
- Real-time Data - Learn about real-time patterns