Integrating GraphQL APIs in Flutter Applications

GraphQL is a query language for your API and a server-side runtime for executing queries. Unlike REST, which typically exposes multiple endpoints, GraphQL allows clients to request specific data they need, nothing more and nothing less. Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, integrates seamlessly with GraphQL APIs, providing an efficient and type-safe way to fetch and manipulate data. This blog post will guide you through integrating GraphQL APIs into your Flutter applications, covering everything from setting up your environment to handling complex queries and mutations.

What is GraphQL and Why Use it with Flutter?

GraphQL solves several common problems with traditional REST APIs:

  • Over-fetching: REST endpoints often return more data than the client needs.
  • Under-fetching: Clients might need to make multiple requests to gather all required data.
  • Versioning: Evolving APIs often require versioning, which can become complex to manage.

By contrast, GraphQL offers:

  • Precise Data Fetching: Clients specify exactly what data they need.
  • Single Endpoint: Simplifies API interactions with a single endpoint.
  • Strongly Typed: Enforces a schema that allows for compile-time checks and tooling.

Integrating GraphQL with Flutter leverages these benefits, improving data fetching efficiency and developer experience.

Setting Up Your Flutter Environment

Before integrating GraphQL, ensure you have a Flutter development environment set up. This includes:

  • Flutter SDK: Install the Flutter SDK from the official Flutter website.
  • Dart SDK: Included with Flutter.
  • IDE: Use VS Code, Android Studio, or any other IDE with Flutter/Dart support.

Create a new Flutter project by running:

flutter create graphql_flutter_app
cd graphql_flutter_app

Adding Dependencies

To interact with GraphQL APIs, you’ll need a GraphQL client library. One of the most popular choices for Flutter is graphql_flutter. Add it to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  graphql_flutter: ^5.1.0

Run flutter pub get to fetch the dependency.

Configuring GraphQL Client

Create a GraphQLClient instance, providing the GraphQL endpoint:

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

void main() {
  final HttpLink httpLink = HttpLink(
    'https://graphqlzero.almansi.me/api', // Replace with your GraphQL endpoint
  );

  final ValueNotifier<GraphQLClient> client = ValueNotifier(
    GraphQLClient(
      link: httpLink,
      cache: GraphQLCache(store: HiveStore()),
    ),
  );

  var app = MaterialApp(
    title: 'Flutter GraphQL App',
    home: GraphQLProvider(
      client: client,
      child: const MyApp(),
    ),
  );

  runApp(app);
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('GraphQL Flutter App'),
      ),
      body: const Center(
        child: Text('GraphQL Integration Example'),
      ),
    );
  }
}

In this setup:

  • HttpLink: Specifies the GraphQL API endpoint.
  • GraphQLClient: Initializes the GraphQL client with the link and cache.
  • GraphQLProvider: Provides the GraphQLClient to all widgets in the app.

Executing GraphQL Queries

To fetch data, use the Query widget. Define your GraphQL query as a string:


query GetPosts {
  posts {
    data {
      id
      title
      body
    }
  }
}

Embed the query within the Query widget to fetch and display the data:

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

class PostList extends StatelessWidget {
  const PostList({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Query(
      options: QueryOptions(
        document: gql(
          '''
          query GetPosts {
            posts {
              data {
                id
                title
                body
              }
            }
          }
          ''',
        ),
      ),
      builder: (QueryResult result, {VoidCallback? refetch, FetchMore? fetchMore}) {
        if (result.hasException) {
          return Text(result.exception.toString());
        }

        if (result.isLoading) {
          return const CircularProgressIndicator();
        }

        List? posts = result.data?['posts']['data'];
        if (posts == null) {
          return const Text('No posts');
        }

        return ListView.builder(
          itemCount: posts.length,
          itemBuilder: (context, index) {
            final post = posts[index];
            return ListTile(
              title: Text(post['title']),
              subtitle: Text(post['body']),
            );
          },
        );
      },
    );
  }
}

This code snippet does the following:

  • Defines a Query widget that executes the GetPosts query.
  • Displays a loading indicator while data is being fetched.
  • Shows an error message if an exception occurs.
  • Renders a list of posts upon successful data retrieval.

Performing Mutations

GraphQL mutations allow you to modify data on the server. Use the Mutation widget for this purpose.


mutation CreatePost($title: String!, $body: String!) {
  createPost(input: { title: $title, body: $body }) {
    id
    title
    body
  }
}

To execute the mutation:

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

class CreatePostForm extends StatefulWidget {
  const CreatePostForm({Key? key}) : super(key: key);

  @override
  _CreatePostFormState createState() => _CreatePostFormState();
}

class _CreatePostFormState extends State<CreatePostForm> {
  final TextEditingController _titleController = TextEditingController();
  final TextEditingController _bodyController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Mutation(
      options: MutationOptions(
        document: gql('''
          mutation CreatePost($title: String!, $body: String!) {
            createPost(input: { title: $title, body: $body }) {
              id
              title
              body
            }
          }
        '''),
        onCompleted: (dynamic result) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Post created!')),
          );
        },
      ),
      builder: (RunMutation runMutation, QueryResult<Object?>? result) {
        return Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            children: [
              TextField(
                controller: _titleController,
                decoration: const InputDecoration(labelText: 'Title'),
              ),
              TextField(
                controller: _bodyController,
                decoration: const InputDecoration(labelText: 'Body'),
              ),
              ElevatedButton(
                onPressed: () {
                  runMutation({
                    'title': _titleController.text,
                    'body': _bodyController.text,
                  });
                },
                child: const Text('Create Post'),
              ),
              if (result?.isLoading ?? false)
                const CircularProgressIndicator(),
            ],
          ),
        );
      },
    );
  }

  @override
  void dispose() {
    _titleController.dispose();
    _bodyController.dispose();
    super.dispose();
  }
}

This widget defines a form with fields for title and body. On pressing the “Create Post” button, it executes the CreatePost mutation with the form values.

Handling Variables

GraphQL queries and mutations often require variables. Pass variables to the options parameter of Query or Mutation:

Query(
  options: QueryOptions(
    document: gql('''
      query GetPostById($id: ID!) {
        post(id: $id) {
          id
          title
          body
        }
      }
    '''),
    variables: {
      'id': '123',
    },
  ),
  builder: (QueryResult result, {VoidCallback? refetch, FetchMore? fetchMore}) {
    // ...
  },
)

The variables map holds the values for the query’s input parameters.

Caching

Effective caching is critical for application performance. The graphql_flutter package includes a caching mechanism.

  • InMemoryCache: Default in-memory cache for simple use cases.
  • HiveStore: Persists the cache using Hive, a lightweight key-value database.

Initialize caching during client setup:

final ValueNotifier<GraphQLClient> client = ValueNotifier(
  GraphQLClient(
    link: httpLink,
    cache: GraphQLCache(store: HiveStore()),
  ),
);

Error Handling

Proper error handling ensures a robust user experience. The QueryResult object returned by the Query and Mutation widgets contains error information:

Query(
  options: QueryOptions(
    document: gql(query),
  ),
  builder: (QueryResult result, {VoidCallback? refetch, FetchMore? fetchMore}) {
    if (result.hasException) {
      return Text('Error: ${result.exception.toString()}');
    }

    // ...
  },
)

Check result.hasException and handle the exception appropriately. Implement retries, logging, or user-friendly error messages as necessary.

Advanced Techniques

For more advanced use cases, consider these techniques:

  • Subscriptions: GraphQL subscriptions provide real-time updates using WebSockets. The graphql_flutter package supports subscriptions.
  • Fragments: Use fragments to reuse common parts of your GraphQL queries.
  • Custom Scalars: Define custom scalar types in your schema and handle them in your Flutter app.

Conclusion

Integrating GraphQL APIs in Flutter applications streamlines data fetching, minimizes over- and under-fetching, and improves developer productivity. Using the graphql_flutter package, you can easily set up a GraphQL client, execute queries and mutations, handle variables, and manage caching. By implementing these techniques, you create robust, efficient, and type-safe Flutter applications that harness the full power of GraphQL.