Flutter GraphQL

Efficient data fetching with GraphQL queries

🔷 What is GraphQL?

GraphQL is a query language for APIs that lets you request exactly the data you need. Unlike REST, you fetch multiple resources in a single request, reducing network calls and improving app performance significantly.


// Simple GraphQL query example
const String getUsers = '''
  query GetUsers {
    users {
      id
      name
      email
    }
  }
''';
                                    

Key GraphQL Concepts

🔍

Queries

Fetch data from the server. Queries let you request specific fields from your API, getting only what you need without over-fetching or under-fetching data.

query {
  user(id: "1") {
    name
    email
  }
}

Mutations

Modify data on the server. Mutations create, update, or delete data, similar to POST, PUT, and DELETE in REST but with more flexibility and precision.

mutation {
  createUser(name: "John", email: "[email protected]") {
    id
    name
  }
}
📡

Subscriptions

Real-time data updates via WebSocket. Subscriptions push data changes to your app instantly, perfect for chat apps, live feeds, or real-time notifications.

subscription {
  messageAdded {
    id
    text
    user
  }
}
🎯

Fragments

Reusable query pieces for cleaner code. Fragments define reusable sets of fields, reducing duplication and making your queries easier to maintain and update.

fragment UserInfo on User {
  id
  name
  email
}

🔹 Setting Up GraphQL

Add the graphql_flutter package to your project:

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  graphql_flutter: ^5.1.0
// main.dart - Initialize GraphQL client
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

void main() async {
  await initHiveForFlutter();
  
  final HttpLink httpLink = HttpLink(
    'https://api.example.com/graphql',
  );

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

  runApp(
    GraphQLProvider(
      client: client,
      child: MyApp(),
    ),
  );
}

Setup steps:

1. Add graphql_flutter to pubspec.yaml

2. Run: flutter pub get

3. Initialize GraphQL client

4. Wrap app with GraphQLProvider

🔹 Fetching Data with Query

Use Query widget to fetch and display data:

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

const String getUsersQuery = '''
  query GetUsers {
    users {
      id
      name
      email
      avatar
    }
  }
''';

class UsersScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Users')),
      body: Query(
        options: QueryOptions(
          document: gql(getUsersQuery),
          pollInterval: Duration(seconds: 10),
        ),
        builder: (QueryResult result, {fetchMore, refetch}) {
          if (result.hasException) {
            return Center(
              child: Text('Error: ${result.exception.toString()}'),
            );
          }

          if (result.isLoading) {
            return Center(child: CircularProgressIndicator());
          }

          List users = result.data!['users'];

          return ListView.builder(
            itemCount: users.length,
            itemBuilder: (context, index) {
              final user = users[index];
              return ListTile(
                leading: CircleAvatar(
                  backgroundImage: NetworkImage(user['avatar']),
                ),
                title: Text(user['name']),
                subtitle: Text(user['email']),
              );
            },
          );
        },
      ),
    );
  }
}

Features:

✓ Automatic loading states

✓ Error handling built-in

✓ Auto-refresh every 10 seconds

✓ Caching for better performance

🔹 Modifying Data with Mutation

Use Mutation widget to create or update data:

const String createUserMutation = '''
  mutation CreateUser(\$name: String!, \$email: String!) {
    createUser(name: \$name, email: \$email) {
      id
      name
      email
    }
  }
''';

class CreateUserScreen extends StatelessWidget {
  final nameController = TextEditingController();
  final emailController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Create User')),
      body: Mutation(
        options: MutationOptions(
          document: gql(createUserMutation),
          onCompleted: (dynamic resultData) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('User created successfully!')),
            );
          },
        ),
        builder: (RunMutation runMutation, QueryResult? result) {
          return Padding(
            padding: EdgeInsets.all(16),
            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: () {
                    runMutation({
                      'name': nameController.text,
                      'email': emailController.text,
                    });
                  },
                  child: result?.isLoading == true
                      ? CircularProgressIndicator(color: Colors.white)
                      : Text('Create User'),
                ),
                if (result?.hasException == true)
                  Padding(
                    padding: EdgeInsets.only(top: 16),
                    child: Text(
                      'Error: ${result!.exception.toString()}',
                      style: TextStyle(color: Colors.red),
                    ),
                  ),
              ],
            ),
          );
        },
      ),
    );
  }
}

What happens:

1. User fills in name and email

2. Mutation sends data to server

3. Server creates new user

4. Success message displayed

🔹 Query with Variables

Pass dynamic values to your queries:

const String getUserQuery = '''
  query GetUser(\$id: ID!) {
    user(id: \$id) {
      id
      name
      email
      posts {
        id
        title
        content
      }
    }
  }
''';

class UserDetailScreen extends StatelessWidget {
  final String userId;

  UserDetailScreen({required this.userId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('User Details')),
      body: Query(
        options: QueryOptions(
          document: gql(getUserQuery),
          variables: {'id': userId},
        ),
        builder: (QueryResult result, {fetchMore, refetch}) {
          if (result.isLoading) {
            return Center(child: CircularProgressIndicator());
          }

          final user = result.data!['user'];

          return SingleChildScrollView(
            padding: EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  user['name'],
                  style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                ),
                Text(user['email']),
                SizedBox(height: 24),
                Text(
                  'Posts',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                ),
                ...List.generate(
                  user['posts'].length,
                  (index) => Card(
                    child: ListTile(
                      title: Text(user['posts'][index]['title']),
                      subtitle: Text(user['posts'][index]['content']),
                    ),
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

🔹 Real-time Updates with Subscription

Listen to real-time data changes:

const String messageSubscription = '''
  subscription OnMessageAdded {
    messageAdded {
      id
      text
      user {
        name
      }
      createdAt
    }
  }
''';

class ChatScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Live Chat')),
      body: Subscription(
        options: SubscriptionOptions(
          document: gql(messageSubscription),
        ),
        builder: (QueryResult result) {
          if (result.hasException) {
            return Center(child: Text('Error: ${result.exception}'));
          }

          if (result.isLoading) {
            return Center(child: Text('Connecting...'));
          }

          final message = result.data!['messageAdded'];

          return ListView(
            children: [
              ListTile(
                title: Text(message['user']['name']),
                subtitle: Text(message['text']),
                trailing: Text(message['createdAt']),
              ),
            ],
          );
        },
      ),
    );
  }
}

Real-time features:

✓ Instant message updates

✓ No manual refresh needed

✓ WebSocket connection

✓ Live data synchronization

🧠 Test Your Knowledge

What is the main advantage of GraphQL over REST?