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 theGraphQLClient
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 theGetPosts
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.