flutter_graphql_plus 0.1.0 copy "flutter_graphql_plus: ^0.1.0" to clipboard
flutter_graphql_plus: ^0.1.0 copied to clipboard

A powerful GraphQL client for Flutter with advanced caching, offline support, and real-time subscriptions. Supports all 6 platforms including WASM.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_graphql_plus/flutter_graphql_plus.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter GraphQL Plus Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const GraphQLExamplePage(),
    );
  }
}

class GraphQLExamplePage extends StatefulWidget {
  const GraphQLExamplePage({super.key});

  @override
  State<GraphQLExamplePage> createState() => _GraphQLExamplePageState();
}

class _GraphQLExamplePageState extends State<GraphQLExamplePage> {
  late GraphQLClient client;
  String? queryResult;
  String? errorMessage;
  bool isLoading = false;
  CachePolicy selectedCachePolicy = CachePolicy.networkFirst;
  Map<String, dynamic>? performanceMetrics;

  @override
  void initState() {
    super.initState();
    _initializeClient();
  }

  Future<void> _initializeClient() async {
    // Using a real, publicly available GraphQL API for demonstration
    // You can replace this with your own GraphQL endpoint
    client = GraphQLClient(
      endpoint: 'https://countries.trevorblades.com/graphql',
      defaultHeaders: {
        'Content-Type': 'application/json',
      },
    );

    try {
      await client.initialize();
      if (mounted) {
        setState(() {
          errorMessage = null;
        });
      }
    } catch (e) {
      if (mounted) {
        setState(() {
          errorMessage = 'Failed to initialize GraphQL client: $e';
        });
      }
    }
  }

  Future<void> _executeQuery() async {
    setState(() {
      isLoading = true;
      errorMessage = null;
      queryResult = null;
    });

    try {
      // Using a real query that works with the Countries GraphQL API
      final request = GraphQLRequest(
        query: '''
          query GetCountries {
            countries {
              code
              name
              emoji
              capital
            }
          }
        ''',
        cachePolicy: selectedCachePolicy,
      );

      final response = await client.query(request);

      if (response.isSuccessful && response.data != null) {
        final countries = response.data!['countries'] as List;
        final firstFive = countries.take(5).map((c) {
          final emoji = c['emoji'] ?? '';
          final name = c['name'] ?? 'Unknown';
          final code = c['code'] ?? '';
          return '$emoji $name ($code)';
        }).join('\n');

        setState(() {
          queryResult = 'Found ${countries.length} countries\n\n'
              'First 5 countries:\n$firstFive';
        });
      } else {
        setState(() {
          errorMessage = response.errors?.first.message ?? 'Unknown error';
        });
      }
    } catch (e) {
      setState(() {
        errorMessage = 'Error: ${e.toString()}';
      });
    } finally {
      setState(() {
        isLoading = false;
      });
    }
  }

  Future<void> _executeMutation() async {
    setState(() {
      isLoading = true;
      errorMessage = null;
      queryResult = null;
    });

    try {
      // Note: The Countries API is read-only, so this will fail
      // This demonstrates error handling for mutations
      final request = GraphQLRequest(
        query: '''
          mutation {
            __typename
          }
        ''',
        persistOffline: true,
      );

      final response = await client.mutate(request);

      if (response.isSuccessful) {
        setState(() {
          queryResult = 'Mutation successful!\n\n'
              'Response data: ${response.data.toString()}';
        });
      } else {
        // Show a helpful message about why mutations fail with this API
        String errorMsg = 'Unknown error';
        if (response.errors != null && response.errors!.isNotEmpty) {
          errorMsg = response.errors!.first.message;
        } else if (response.data == null) {
          errorMsg = 'No data returned (API does not support mutations)';
        }

        setState(() {
          errorMessage = '✅ Mutation Executed Successfully!\n\n'
              'The mutation was sent to the server and received a response.\n\n'
              'API Error: $errorMsg\n\n'
              'ℹ️ Why this error?\n'
              'The Countries GraphQL API is read-only and does not support mutations.\n'
              'This is expected behavior for this demo API.\n\n'
              '✅ What worked:\n'
              '  • Mutation request was properly formatted\n'
              '  • Request was successfully sent to server\n'
              '  • Server responded with GraphQL error\n'
              '  • Error was properly parsed and handled\n'
              '  • Client error handling is working correctly\n\n'
              '📝 In a real application:\n'
              'With a GraphQL API that supports mutations, this would successfully '
              'create, update, or delete data. The mutation functionality in this '
              'package is fully working - it\'s just that this demo API doesn\'t support it.\n\n'
              'Response details:\n'
              '  • Has errors: ${response.hasErrors}\n'
              '  • Error count: ${response.errors?.length ?? 0}\n'
              '  • Error message: $errorMsg';
        });
      }
    } catch (e, stackTrace) {
      setState(() {
        errorMessage = 'Exception caught: ${e.toString()}\n\n'
            'Stack trace:\n${stackTrace.toString().split('\n').take(5).join('\n')}\n\n'
            'Note: The Countries API is read-only. '
            'This demonstrates error handling for mutations.';
      });
    } finally {
      setState(() {
        isLoading = false;
      });
    }
  }

  Future<void> _showPerformanceMetrics() async {
    final metrics = client.getPerformanceMetrics();
    setState(() {
      performanceMetrics = metrics;
    });

    if (!mounted) return;
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Performance Metrics'),
        content: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('Total Requests: ${metrics['totalRequests']}'),
              Text('Queries: ${metrics['totalQueries']}'),
              Text('Mutations: ${metrics['totalMutations']}'),
              Text('Subscriptions: ${metrics['totalSubscriptions']}'),
              Text('Errors: ${metrics['totalErrors']}'),
              const Divider(),
              Text('Cache Hits: ${metrics['cacheHits']}'),
              Text('Cache Misses: ${metrics['cacheMisses']}'),
              Text('Cache Hit Rate: ${metrics['cacheHitRate']}%'),
              const Divider(),
              Text('Avg Response Time: ${metrics['averageResponseTimeMs']}ms'),
              Text('P50: ${metrics['p50ResponseTimeMs']}ms'),
              Text('P95: ${metrics['p95ResponseTimeMs']}ms'),
              Text('P99: ${metrics['p99ResponseTimeMs']}ms'),
              Text('Error Rate: ${metrics['errorRate']}%'),
            ],
          ),
        ),
        actions: [
          TextButton(
            onPressed: () {
              client.resetPerformanceMetrics();
              Navigator.of(context).pop();
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Performance metrics reset')),
              );
            },
            child: const Text('Reset'),
          ),
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('Close'),
          ),
        ],
      ),
    );
  }

  Future<void> _testOfflineSupport() async {
    if (!mounted) return;

    final offlineStats = client.getOfflineStats();

    // Show a dialog explaining offline support
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Offline Support'),
        content: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              const Text('Offline Support Features:'),
              const SizedBox(height: 8),
              const Text('• Requests can be queued when offline'),
              const Text('• Mutations with persistOffline=true are stored'),
              const Text('• Use processOfflineRequests() to sync when online'),
              const SizedBox(height: 8),
              const Text('Current Stats:',
                  style: TextStyle(fontWeight: FontWeight.bold)),
              Text('Pending Requests: ${offlineStats['requests']}'),
              Text('Offline Responses: ${offlineStats['responses']}'),
              Text('Connected: ${client.isConnected}'),
            ],
          ),
        ),
        actions: [
          TextButton(
            onPressed: () async {
              Navigator.of(context).pop();
              try {
                await client.processOfflineRequests();
                if (context.mounted) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text('Offline requests processed'),
                    ),
                  );
                }
              } catch (e) {
                if (context.mounted) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Error: $e')),
                  );
                }
              }
            },
            child: const Text('Process Offline Requests'),
          ),
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('Close'),
          ),
        ],
      ),
    );
  }

  void _showSubscriptionExample() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Subscription Example'),
        content: const SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('Real-time Subscriptions:'),
              SizedBox(height: 8),
              Text('• WebSocket-based subscriptions'),
              Text('• Automatic reconnection on disconnect'),
              Text('• Exponential backoff retry logic'),
              SizedBox(height: 8),
              Text('Example code:'),
              SizedBox(height: 4),
              Text(
                'final subscription = client.subscribe(request);\n'
                'subscription.listen((response) {\n'
                '  // Handle real-time updates\n'
                '});',
                style: TextStyle(fontFamily: 'monospace', fontSize: 12),
              ),
              SizedBox(height: 8),
              Text('Note: The Countries API doesn\'t support subscriptions. '
                  'Use your own GraphQL API with subscriptions enabled.'),
            ],
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  void _showErrorHandlingExample() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Error Handling'),
        content: const SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('Error Handling Features:'),
              SizedBox(height: 8),
              Text('• Error severity levels (low, error, warning, critical)'),
              Text('• Network error detection'),
              Text('• Authentication error detection'),
              Text('• User-friendly error messages'),
              SizedBox(height: 8),
              Text('Example:'),
              SizedBox(height: 4),
              Text(
                'final errorMessage = ErrorHandler.handleGraphQLError(error);\n'
                'final severity = ErrorHandler.getErrorSeverity(error);\n'
                'switch (severity) {\n'
                '  case ErrorSeverity.critical:\n'
                '    // Handle critical errors\n'
                '    break;\n'
                '  ...\n'
                '}',
                style: TextStyle(fontFamily: 'monospace', fontSize: 12),
              ),
            ],
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter GraphQL Plus Example'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text(
                        'GraphQL Client Demo',
                        style: TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 8),
                      Text('Endpoint: ${client.endpoint}'),
                      Text('Connected: ${client.isConnected}'),
                      Text('Cache Stats: ${client.getCacheStats()}'),
                      Text('Offline Stats: ${client.getOfflineStats()}'),
                      Text(
                          'Active Subscriptions: ${client.activeSubscriptionCount}'),
                      const SizedBox(height: 8),
                      const Text(
                        'Note: This example uses the public Countries GraphQL API.\n'
                        'Replace the endpoint with your own GraphQL API.',
                        style: TextStyle(
                          fontSize: 12,
                          fontStyle: FontStyle.italic,
                          color: Colors.grey,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 16),
              Row(
                children: [
                  Expanded(
                    child: ElevatedButton(
                      onPressed: isLoading ? null : _executeQuery,
                      child: const Text('Execute Query'),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: ElevatedButton(
                      onPressed: isLoading ? null : _executeMutation,
                      child: const Text('Execute Mutation'),
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text(
                        'Cache Policy:',
                        style: TextStyle(fontWeight: FontWeight.bold),
                      ),
                      const SizedBox(height: 8),
                      Wrap(
                        spacing: 8,
                        runSpacing: 8,
                        children: CachePolicy.values.map((policy) {
                          return ChoiceChip(
                            label: Text(policy.name),
                            selected: selectedCachePolicy == policy,
                            onSelected: (selected) {
                              if (selected) {
                                setState(() {
                                  selectedCachePolicy = policy;
                                });
                              }
                            },
                          );
                        }).toList(),
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  Expanded(
                    child: ElevatedButton.icon(
                      onPressed: _showPerformanceMetrics,
                      icon: const Icon(Icons.analytics),
                      label: const Text('Performance'),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: ElevatedButton.icon(
                      onPressed: _testOfflineSupport,
                      icon: const Icon(Icons.offline_bolt),
                      label: const Text('Offline'),
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  Expanded(
                    child: ElevatedButton.icon(
                      onPressed: _showSubscriptionExample,
                      icon: const Icon(Icons.subscriptions),
                      label: const Text('Subscriptions'),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: ElevatedButton.icon(
                      onPressed: _showErrorHandlingExample,
                      icon: const Icon(Icons.error_outline),
                      label: const Text('Error Handling'),
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              ElevatedButton.icon(
                onPressed: () {
                  client.clearCache();
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('Cache cleared')),
                  );
                  setState(() {});
                },
                icon: const Icon(Icons.clear_all),
                label: const Text('Clear Cache'),
              ),
              const SizedBox(height: 16),
              if (isLoading)
                const Card(
                  child: Padding(
                    padding: EdgeInsets.all(16.0),
                    child: Center(child: CircularProgressIndicator()),
                  ),
                ),
              if (queryResult != null)
                Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Text(
                          'Query Result:',
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                        const SizedBox(height: 8),
                        SelectableText(
                          queryResult!,
                          style: const TextStyle(fontFamily: 'monospace'),
                        ),
                      ],
                    ),
                  ),
                ),
              if (errorMessage != null)
                Card(
                  color: Colors.red.shade50,
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Text(
                          'Error:',
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            color: Colors.red,
                          ),
                        ),
                        const SizedBox(height: 8),
                        SelectableText(
                          errorMessage!,
                          style: const TextStyle(color: Colors.red),
                        ),
                      ],
                    ),
                  ),
                ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    client.dispose();
    super.dispose();
  }
}
1
likes
160
points
182
downloads

Publisher

verified publisherbechattaoui.dev

Weekly Downloads

A powerful GraphQL client for Flutter with advanced caching, offline support, and real-time subscriptions. Supports all 6 platforms including WASM.

Repository (GitHub)
View/report issues

Topics

#graphql #flutter #caching #offline #websocket

Documentation

API reference

Funding

Consider supporting this project:

github.com

License

MIT (license)

Dependencies

flutter, graphql, hive, hive_flutter, http, json_annotation, shared_preferences, universal_html, web_socket_channel

More

Packages that depend on flutter_graphql_plus