GraphQL has emerged as a powerful alternative to REST for building APIs. Its flexible nature allows clients to request only the data they need, avoiding over-fetching and improving performance. Flutter, with its reactive framework and rich ecosystem, is an excellent choice for building applications that consume GraphQL APIs. In this comprehensive guide, we’ll explore how to work with GraphQL APIs in Flutter, covering everything from setting up dependencies to handling complex queries and mutations.
What is GraphQL?
GraphQL is a query language for your API and a server-side runtime for executing queries by using a type system you define for your data. Unlike REST, which uses fixed data structures defined by the server, GraphQL enables clients to request specific data points.
Why Use GraphQL with Flutter?
- Data Efficiency: Request only the data you need, reducing payload sizes.
- Schema Introspection: Easily understand the API structure using GraphQL’s introspection capabilities.
- Strong Typing: Leverage the GraphQL schema for compile-time type checking.
- Real-time Updates: Support for subscriptions enables real-time data updates.
Setting Up Your Flutter Project
Before diving into the code, make sure you have Flutter installed and set up on your development machine.
Step 1: Create a New Flutter Project
Open your terminal and run:
flutter create flutter_graphql_app
Then, navigate into your project directory:
cd flutter_graphql_app
Step 2: Add Dependencies
You’ll need a GraphQL client to interact with GraphQL APIs. The most popular GraphQL client 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 install the dependencies.
Making Your First GraphQL Query
Let’s start by making a simple GraphQL query to fetch some data.
Step 1: Initialize GraphQL Client
In your main app file (lib/main.dart), initialize the GraphQL client:
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
void main() {
final HttpLink httpLink = HttpLink(
'https://rickandmortyapi.graphcdn.app/', // Replace with your GraphQL endpoint
);
final ValueNotifier client = ValueNotifier(
GraphQLClient(
link: httpLink,
cache: GraphQLCache(store: InMemoryStore()),
),
);
var app = MaterialApp(
title: 'Flutter GraphQL Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GraphQLProvider(
client: client,
child: MyApp(),
),
);
runApp(app);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GraphQL Demo'),
),
body: Center(
child: Text('Fetching data...'),
),
);
}
}
Here, we:
- Import the necessary packages from
flutterandgraphql_flutter. - Create an
HttpLinkwith the GraphQL endpoint (replace with your own). - Initialize the
GraphQLClientwith the link and an in-memory cache. - Wrap our
MyAppwithGraphQLProviderto make the client accessible to the app.
Step 2: Define Your GraphQL Query
Create a query string:
const String readCharacters = """
query GetCharacters {
characters {
results {
id
name
}
}
}
""";
Step 3: Use Query Widget to Fetch Data
Wrap your widget with the Query widget from graphql_flutter to execute the query:
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
void main() {
final HttpLink httpLink = HttpLink(
'https://rickandmortyapi.graphcdn.app/', // Replace with your GraphQL endpoint
);
final ValueNotifier client = ValueNotifier(
GraphQLClient(
link: httpLink,
cache: GraphQLCache(store: InMemoryStore()),
),
);
var app = MaterialApp(
title: 'Flutter GraphQL Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GraphQLProvider(
client: client,
child: MyApp(),
),
);
runApp(app);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GraphQL Demo'),
),
body: Query(
options: QueryOptions(
document: gql(readCharacters),
),
builder: (QueryResult result, {Refetch? refetch, FetchMore? fetchMore}) {
if (result.hasException) {
return Text(result.exception.toString());
}
if (result.isLoading) {
return Center(child: CircularProgressIndicator());
}
final characterList = result.data?['characters']['results'];
if (characterList == null || characterList is! List) {
return Text('No characters found');
}
return ListView.builder(
itemCount: characterList.length,
itemBuilder: (context, index) {
final character = characterList[index];
return ListTile(
title: Text(character['name'] ?? 'Unknown Name'),
);
},
);
},
),
);
}
}
In this code:
- The
Querywidget executes the GraphQL query defined inreadCharacters. - The
builderfunction handles the result, loading state, and errors. - It displays a list of characters fetched from the API.
Making GraphQL Mutations
GraphQL mutations allow you to modify data on the server. Let’s look at how to create a mutation in Flutter.
Step 1: Define Your GraphQL Mutation
Define a mutation string:
const String createTodo = """
mutation CreateTodo($title: String!, $completed: Boolean!) {
createTodo(title: $title, completed: $completed) {
id
title
completed
}
}
""";
Step 2: Use Mutation Widget to Perform the Mutation
Wrap your widget with the Mutation widget from graphql_flutter:
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
void main() {
final HttpLink httpLink = HttpLink(
'YOUR_GRAPHQL_ENDPOINT', // Replace with your GraphQL endpoint
);
final ValueNotifier client = ValueNotifier(
GraphQLClient(
link: httpLink,
cache: GraphQLCache(store: InMemoryStore()),
),
);
var app = MaterialApp(
title: 'Flutter GraphQL Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GraphQLProvider(
client: client,
child: TodoApp(),
),
);
runApp(app);
}
class TodoApp extends StatefulWidget {
@override
_TodoAppState createState() => _TodoAppState();
}
class _TodoAppState extends State {
final TextEditingController _titleController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GraphQL Todo'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _titleController,
decoration: InputDecoration(labelText: 'Todo Title'),
),
Mutation(
options: MutationOptions(
document: gql(createTodo),
onCompleted: (dynamic resultData) {
print('Todo Created: $resultData');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Todo created!')),
);
},
),
builder: (RunMutation runMutation, QueryResult? result) {
return ElevatedButton(
onPressed: () {
runMutation({
'title': _titleController.text,
'completed': false,
});
},
child: Text('Create Todo'),
);
},
),
],
),
),
);
}
@override
void dispose() {
_titleController.dispose();
super.dispose();
}
}
In this code:
- The
Mutationwidget wraps theElevatedButton. - The
builderprovides arunMutationfunction to execute the mutation. - The
onPressedhandler callsrunMutationwith the necessary variables (titleandcompleted). - The
onCompletedcallback handles the response after a successful mutation.
Handling Real-time Data with Subscriptions
GraphQL subscriptions allow you to receive real-time updates from the server. The graphql_flutter package supports subscriptions using WebSockets.
Step 1: Configure WebSocket Link
Instead of HttpLink, use WebSocketLink:
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
void main() async {
await initHiveForFlutter(); // Required for caching
final WebSocketLink websocketLink = WebSocketLink(
'ws://YOUR_GRAPHQL_ENDPOINT/subscriptions', // Replace with your GraphQL subscription endpoint
);
final ValueNotifier client = ValueNotifier(
GraphQLClient(
link: websocketLink,
cache: GraphQLCache(store: HiveStore()),
),
);
var app = MaterialApp(
title: 'Flutter GraphQL Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GraphQLProvider(
client: client,
child: SubscriptionApp(),
),
);
runApp(app);
}
class SubscriptionApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
const String subscribeToTodos = """
subscription {
todoCreated {
id
title
completed
}
}
""";
return Scaffold(
appBar: AppBar(
title: Text('GraphQL Subscriptions'),
),
body: Subscription(
options: SubscriptionOptions(document: gql(subscribeToTodos)),
builder: (BuildContext context, result, {refetch}) {
if (result.hasException) {
return Text(result.exception.toString());
}
if (result.isLoading) {
return Center(child: CircularProgressIndicator());
}
final todo = result.data?['todoCreated'];
if (todo == null) {
return Text('No new todos');
}
return ListTile(
title: Text(todo['title'] ?? 'New Todo'),
);
},
),
);
}
}
Key points:
- Ensure that your GraphQL server supports subscriptions over WebSockets.
- Replace
'ws://YOUR_GRAPHQL_ENDPOINT/subscriptions'with the correct WebSocket endpoint.
Advanced Techniques
1. Error Handling
GraphQL APIs can return errors in the errors field of the response. You can handle these errors in the builder functions of Query, Mutation, and Subscription widgets:
Query(
options: QueryOptions(document: gql(readCharacters)),
builder: (QueryResult result, {Refetch? refetch, FetchMore? fetchMore}) {
if (result.hasException) {
print('GraphQL Errors: $result.exception.toString()');
return Text('Error fetching data');
}
// ... rest of your code
},
)
2. Pagination
For large datasets, implement pagination to fetch data in smaller chunks:
const String readCharactersWithPagination = """
query GetCharacters($page: Int) {
characters(page: $page) {
results {
id
name
}
info {
count
pages
next
}
}
}
""";
Use variables to control the page and fetch more data as needed.
3. Caching
The graphql_flutter package supports caching out-of-the-box. You can configure different caching strategies, such as in-memory cache, or persistent cache using Hive:
import 'package:hive_flutter/hive_flutter.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
void main() async {
await initHiveForFlutter();
final ValueNotifier client = ValueNotifier(
GraphQLClient(
link: HttpLink('YOUR_GRAPHQL_ENDPOINT'),
cache: GraphQLCache(store: HiveStore()), // Using Hive for caching
),
);
runApp(
GraphQLProvider(
client: client,
child: MyApp(),
),
);
}
Best Practices
- Keep Queries Concise: Only request the data your UI needs to optimize performance.
- Handle Errors Gracefully: Provide user-friendly error messages and logging.
- Use Caching: Cache frequently accessed data to reduce network requests.
- Normalize Data: Organize fetched data for easy access in your UI.
Conclusion
Working with GraphQL APIs in Flutter offers a powerful and efficient way to build modern, data-driven applications. By leveraging the graphql_flutter package, you can easily perform queries, mutations, and subscriptions, unlocking the full potential of GraphQL’s flexibility and performance benefits. With these comprehensive steps and examples, you’re well-equipped to integrate GraphQL into your Flutter projects.