Using GraphQL in Flutter Apps

This using graphql in flutter apps is posted by seven.srikanth at 5/2/2025 11:40:55 PM



<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 &#39;package:flutter/material.dart&#39;; import &#39;package:graphql_flutter/graphql_flutter.dart&#39;; import &#39;package:hive_flutter/hive_flutter.dart&#39;;

void main() async { WidgetsFlutterBinding.ensureInitialized(); await Hive.initFlutter(); await Hive.openBox(&#39;graphql_cache&#39;);

final HttpLink httpLink = HttpLink( &#39;https://your-graphql-endpoint.com/graphql&#39;, defaultHeaders: { &#39;Authorization&#39;: &#39;Bearer your-token&#39;, }, );

final AuthLink authLink = AuthLink( getToken: () async =&gt; &#39;Bearer your-token&#39;, );

final Link link = authLink.concat(httpLink);

final ValueNotifier&lt;GraphQLClient&gt; 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&#39;&#39;&#39; query GetUsers($first: Int, $after: String) { users(first: $first, after: $after) { edges { node { id name email posts { id title } } cursor } pageInfo { hasNextPage endCursor } } } &#39;&#39;&#39;;

@override Widget build(BuildContext context) { return Query( options: QueryOptions( document: gql(getUsers), variables: { &#39;first&#39;: 10, &#39;after&#39;: 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?[&amp;#39;users&amp;#39;]?[&amp;#39;edges&amp;#39;] ?? [];
    final pageInfo = result.data?[&amp;#39;users&amp;#39;]?[&amp;#39;pageInfo&amp;#39;];

    return ListView.builder(
      itemCount: users.length + (pageInfo[&amp;#39;hasNextPage&amp;#39;] ? 1 : 0),
      itemBuilder: (context, index) {
        if (index == users.length) {
          return ElevatedButton(
            onPressed: () {
              fetchMore!(
                FetchMoreOptions(
                  variables: {
                    &amp;#39;after&amp;#39;: pageInfo[&amp;#39;endCursor&amp;#39;],
                  },
                  updateQuery: (previous, next) {
                    final List&amp;lt;dynamic&amp;gt; repos = [
                      ...previous?[&amp;#39;users&amp;#39;]?[&amp;#39;edges&amp;#39;] ?? [],
                      ...next?[&amp;#39;users&amp;#39;]?[&amp;#39;edges&amp;#39;] ?? [],
                    ];
                    next?[&amp;#39;users&amp;#39;]?[&amp;#39;edges&amp;#39;] = repos;
                    return next;
                  },
                ),
              );
            },
            child: const Text(&amp;#39;Load More&amp;#39;),
          );
        }

        final user = users[index][&amp;#39;node&amp;#39;];
        return ListTile(
          title: Text(user[&amp;#39;name&amp;#39;]),
          subtitle: Text(user[&amp;#39;email&amp;#39;]),
          trailing: Text(&amp;#39;${user[&amp;#39;posts&amp;#39;]?.length ?? 0} posts&amp;#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&#39;&#39;&#39; mutation AddUser($input: CreateUserInput!) { addUser(input: $input) { id name email posts { id title } } } &#39;&#39;&#39;;

@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![&#39;addUser&#39;]; final normalizedId = cache.identify(newUser)!; cache.writeQuery( QueryOptions( document: gql(getUsers), ), data: { &#39;users&#39;: { &#39;edges&#39;: [ { &#39;node&#39;: newUser, &#39;cursor&#39;: normalizedId, }, ], }, }, ); } }, onCompleted: (dynamic resultData) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text(&#39;User added successfully&#39;)), ); }, onError: (OperationException? error) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(error.toString())), ); }, ), builder: (RunMutation runMutation, QueryResult? result) { final formKey = GlobalKey&lt;FormState&gt;(); final nameController = TextEditingController(); final emailController = TextEditingController();

    return Form(
      key: formKey,
      child: Column(
        children: [
          TextFormField(
            controller: nameController,
            decoration: const InputDecoration(labelText: &amp;#39;Name&amp;#39;),
            validator: (value) {
              if (value?.isEmpty ?? true) {
                return &amp;#39;Please enter a name&amp;#39;;
              }
              return null;
            },
          ),
          TextFormField(
            controller: emailController,
            decoration: const InputDecoration(labelText: &amp;#39;Email&amp;#39;),
            validator: (value) {
              if (value?.isEmpty ?? true) {
                return &amp;#39;Please enter an email&amp;#39;;
              }
              return null;
            },
          ),
          ElevatedButton(
            onPressed: () {
              if (formKey.currentState?.validate() ?? false) {
                runMutation({
                  &amp;#39;input&amp;#39;: {
                    &amp;#39;name&amp;#39;: nameController.text,
                    &amp;#39;email&amp;#39;: emailController.text,
                  },
                });
              }
            },
            child: const Text(&amp;#39;Add User&amp;#39;),
          ),
        ],
      ),
    );
  },
);

} } </pre> <h2 id="implementing-subscriptions">Implementing Subscriptions</h2> <h3 id="websocket-setup">1. WebSocket Setup</h3> <pre>final WebSocketLink websocketLink = WebSocketLink( url: &#39;wss://your-graphql-endpoint.com/graphql&#39;, config: SocketClientConfig( autoReconnect: true, inactivityTimeout: const Duration(seconds: 30), initialPayload: () async =&gt; { &#39;headers&#39;: { &#39;Authorization&#39;: &#39;Bearer your-token&#39;, }, }, ), );

final Link link = Link.split( (request) =&gt; 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&#39;&#39;&#39; subscription OnUserUpdate { userUpdates { id name email status } } &#39;&#39;&#39;;

@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?[&amp;#39;userUpdates&amp;#39;] ?? [];
    return ListView.builder(
      itemCount: updates.length,
      itemBuilder: (context, index) {
        final user = updates[index];
        return ListTile(
          title: Text(user[&amp;#39;name&amp;#39;]),
          subtitle: Text(user[&amp;#39;email&amp;#39;]),
          trailing: Text(user[&amp;#39;status&amp;#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() ?? &amp;#39;An error occurred&amp;#39;;

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: Text(message),
    action: SnackBarAction(
      label: &amp;#39;Retry&amp;#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&lt;Response&gt; request(Request request, [NextLink? forward]) async* { int attempts = 0; while (true) { try { yield* forward!(request); break; } catch (e) { attempts++; if (attempts &gt;= maxRetries) rethrow; await Future.delayed(retryInterval); } } } } </pre> <h3 id="cache-management">2. Cache Management</h3> <pre>class CacheManager { static Future&lt;void&gt; clearCache() async { final box = await Hive.openBox(&#39;graphql_cache&#39;); await box.clear(); }

static Future&lt;void&gt; updateCache(String query, Map&lt;String, dynamic&gt; data) async { final box = await Hive.openBox(&#39;graphql_cache&#39;); await box.put(query, data); }

static Future&lt;Map&lt;String, dynamic&gt;?&gt; getFromCache(String query) async { final box = await Hive.openBox(&#39;graphql_cache&#39;); 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>


Tags: flutter,markdown,generated








0 Comments
Login to comment.
Recent Comments