Powering Flutter Apps with GraphQL: Unleashing Efficient Data Queries

Payam Asefi
13 min readFeb 17, 2024

--

In the ever-evolving landscape of mobile app development, the combination of Flutter and GraphQL stands out as a dynamic duo, offering developers a seamless experience in building powerful and responsive applications. Before we dive into the practical aspects of integrating GraphQL into Flutter, let’s take a moment to explore the fundamental concepts of GraphQL, understand its distinctions from traditional REST APIs, and grasp the essence of queries and mutations.

Section 1: Unraveling GraphQL — A Developer’s Guide

Introduction to GraphQL:

  • GraphQL, at its core, is a query language for APIs, providing a more efficient and flexible alternative to traditional REST APIs. It was developed by Facebook and later open-sourced.
  • Unlike REST, where endpoints dictate the structure of data, GraphQL allows clients to specify the exact data they need, minimizing over-fetching and under-fetching of data.

Key Differences from REST API:

  • In a REST API, each endpoint typically represents a specific resource, and the server defines the structure of the response. In contrast, GraphQL has a single endpoint, and clients can request precisely the data they require.
  • GraphQL allows for multiple data requests in a single query, reducing the number of API calls required for a particular operation.

Understanding Queries and Mutations:

  • Queries: Queries are the primary means of fetching data from a GraphQL server. They define the structure of the expected response and allow clients to request specific fields. This flexibility in querying empowers developers to receive exactly the data they need, eliminating over-fetching.
  • Mutations: Mutations, on the other hand, are employed to modify data on the server. They facilitate actions such as creating, updating, or deleting data. Mutations establish a two-way communication channel between the client and the server, ensuring that changes are accurately reflected on both ends.

Example Query:

Let’s take a closer look at the specific query we’ll be using in our Flutter and GraphQL integration:

query ($page: Int, $perPage: Int) {
Page(page: $page, perPage: $perPage) {
pageInfo {
total
currentPage
lastPage
hasNextPage
perPage
}
media {
id
coverImage {
extraLarge
}
title {
english
native
}
description
}
}
}

In this query, we request data from a Page using variables $page and $perPage. The response includes information about pagination (pageInfo) and details about each media item, such as id, coverImage, title, and description. This query serves as the backbone of our interaction with the GraphQL server as we fetch anime data for our Flutter application.

Stay tuned as we delve deeper into the practical aspects of using GraphQL with Flutter, starting with crafting your first dynamic query to fetch data from the AniList API.

Section 2: Building the Foundation with GraphQL and Flutter

Dependencies Setup:

In this section, we’ll kickstart our project by adding the essential dependencies to our pubspec.yaml file.

dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.4
graphql: ^5.1.3

Save the file and run flutter pub get to fetch and install the dependencies.

Configuring GraphQL Client:

To connect to a GraphQL server, we’ll create a dedicated GraphQLService class. This class will encapsulate the configuration and management of the GraphQL client. Open a new file, say graphql_service.dart, and define the following:

// graphql_service.dart
import 'package:graphql/client.dart';

class GraphQLService {
static const String _endpoint = 'https://graphql.anilist.co';
final GraphQLClient client;

GraphQLService({GraphQLClient? injectedClient})
: client = injectedClient ?? _buildClient();

static GraphQLClient _buildClient() {
final Link link = HttpLink(_endpoint);

return GraphQLClient(
link: link,
cache: GraphQLCache(),
);
}

Future<QueryResult> query(
String queryString, {
Map<String, dynamic>? variables,
}) async {
try {
_buildClient();
final QueryResult result = await client.query(
QueryOptions(
document: gql(queryString),
variables: variables ?? {},
),
);

if (result.hasException) {
throw result.exception!;
}

return result;
} catch (e) {
throw e.toString();
}
}
}

Understanding GraphQLClient:

GraphQLClient serves as our main conduit for executing GraphQL operations within the Dart package. It acts as the gateway, responsible for sending queries and mutations to a GraphQL server, handling responses, and managing the entire interaction process. In our GraphQLService, we create an instance of GraphQLClient through the _buildClient() method, configuring it with a specific endpoint (_endpoint) and a cache mechanism (GraphQLCache()) to optimize our GraphQL queries.

Understanding Link:

Link is a critical concept that establishes the connection between our Dart application and the GraphQL server. Here, we utilize the HttpLink, indicating that our GraphQL communication occurs over HTTP. The HttpLink manages the connection and facilitates the transportation of GraphQL queries and mutations between our Flutter app and the GraphQL server.

Understanding QueryOptions:

QueryOptions encapsulates the options for a GraphQL query or mutation. It contains details such as the GraphQL document, variables required by the query, and other execution-related options. In our GraphQLService's query method, we use QueryOptions to define the specifics of our GraphQL query. This involves setting the document to the parsed GraphQL query string, providing necessary variables, and configuring other options.

Understanding QueryResult:

QueryResult represents the result of executing a GraphQL query or mutation. In our GraphQLService, after sending a query using GraphQLClient, we await the result. If the query encounters an exception, we log it, ensuring a robust error-handling mechanism.

In summary, GraphQLClient, Link, QueryOptions, and QueryResult collaboratively enable communication between our Flutter app and the GraphQL server. The client manages the overall GraphQL interaction, the link establishes the connection, the query options define the specifics of each GraphQL operation, and the result is captured and processed through QueryResult .

Integrating BLoC(Cubit):

Create a directory named anime within your Flutter project to house classes responsible for state management. Inside this directory, craft two essential files: anime_cubit.dart and anime_state.dart. For now they are empty but we will fill them soon.

// anime_cubit.dart
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

part 'anime_state.dart';

class AnimeCubit extends Cubit<AnimeState> {
AnimeCubit() : super(AnimeInitial());
}
// anime_state.dart
part of 'anime_cubit.dart';

@immutable
abstract class AnimeState {}

class AnimeInitial extends AnimeState {}

class AnimeLoading extends AnimeState {}

class AnimeContent extends AnimeState {}

class AnimeError extends AnimeState {
AnimeError(this.errorMessage);

final String errorMessage;
}

In the file anime_cubit.dart, think of AnimeCubit like the decision-maker for our app. It figures out how the app should act when things happen. Starting with a default state called AnimeInitial, it's the go-to manager for handling user actions and changes in data.

Now, meet its buddy, anime_state.dart. This file defines the different moods our app can have—like when it's loading something, showing content, or facing a problem. By keeping things organized, we make sure our Flutter app smoothly reacts to what users do, making it strong and quick to respond.

Section 3: Updating our Cubit

Before diving into the integration of the GraphQL query into our AnimeCubit, let's define a model that aligns with the structure of the data returned by the query. This model will facilitate the seamless mapping of GraphQL responses to Dart objects.

Developing the Anime Model:

  • Create a new Dart file, say anime_model.dart.
  • Define a class named AnimeModel with properties mirroring the fields in the GraphQL query.
  • Implement a factory constructor to efficiently convert JSON data to Dart objects.

Here is a sample implementation:

// anime_model.dart

class AnimeModel {
final PageInfo pageInfo;
final List<Media> media;

AnimeModel({required this.pageInfo, required this.media});

factory AnimeModel.fromJson(Map<String, dynamic> json) {
return AnimeModel(
pageInfo: PageInfo.fromJson(json['pageInfo']),
media: Media.fromListJson(json['media']),
);
}
}

class PageInfo {
PageInfo({
required this.total,
required this.currentPage,
required this.lastPage,
required this.hasNextPage,
required this.perPage,
});

factory PageInfo.fromJson(Map<String, dynamic> json) {
return PageInfo(
total: json['total'] as int,
currentPage: json['currentPage'] as int,
lastPage: json['lastPage'] as int,
hasNextPage: json['hasNextPage'] as bool,
perPage: json['perPage'] as int,
);
}

final int total;
final int currentPage;
final int lastPage;
final bool hasNextPage;
final int perPage;
}

class Media {
Media({
required this.id,
this.coverImageUrl,
this.englishTitle,
this.nativeTitle,
this.description,
});

static List<Media> fromListJson(List<Object?> jsonList) {
return jsonList
.map((item) => Media.fromJson(item as Map<String, dynamic>))
.toList();
}

factory Media.fromJson(Map<String, dynamic> json) {
return Media(
id: json['id'] as int,
coverImageUrl: json['coverImage']['extraLarge'] as String?,
englishTitle: json['title']['english'] as String?,
nativeTitle: json['title']['native'] as String?,
description: json['description'] as String?,
);
}

final int id;
final String? coverImageUrl;
final String? englishTitle;
final String? nativeTitle;
final String? description;
}

The AnimeModel class serves as a comprehensive representation of the data returned from our GraphQL query. It comprises a PageInfo object, containing details about the pagination, such as the total number of items, the current page, the last page, whether there is a next page, and the number of items per page.

Additionally, it encapsulates a list of Media objects, where each Media instance represents information about a specific anime. The Media class includes essential details such as the anime's unique identifier (id), cover image URL, English and native titles, and a brief description.

Updating AnimeCubit for GraphQL Integration:

Now that we have our AnimeModel ready to represent the GraphQL data, let's seamlessly integrate the GraphQL query into our AnimeCubit and ensure that our BLoC can efficiently handle the new GraphQL-based data source.

// anime_state.dart
part of 'anime_cubit.dart';

@immutable
abstract class AnimeState {}

class AnimeInitial extends AnimeState {}

class AnimeLoading extends AnimeState {}

class AnimeContent extends AnimeState {
AnimeContent(this.animeList);

final List<Media> animeList;
}

class AnimeError extends AnimeState {
AnimeError(this.errorMessage);

final String errorMessage;
}
// anime_cubit.dart
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

part 'anime_state.dart';

class AnimeCubit extends Cubit<AnimeState> {
AnimeCubit({required this.graphQLService}) : super(AnimeInitial());

final GraphQLService graphQLService; // Your GraphQL service instance

// GraphQL query string
static const String _animeQuery = r'''
query ($page: Int, $perPage: Int) {
Page (page: $page, perPage: $perPage) {
pageInfo {
total
currentPage
lastPage
hasNextPage
perPage
}
media {
id
coverImage{
extraLarge
}
title {
english
native
}
description
}
}
}
''';
static const int _perPage = 20;
final List<Media> _animeList = [];
var _currentPage = 1;
var _totalPages = 1;

// Method to fetch anime data using GraphQL query
Future<void> fetchAnimeData({bool showLoading = false}) async {
try {
if (showLoading) {
emit(AnimeLoading()); // Loading state
}

final result = await graphQLService.query(
_animeQuery, // Use the static query string
variables: {'page': _currentPage, 'perPage': _perPage},
);

if (result.hasException) {
emit(AnimeError(result.exception?.graphqlErrors.first.message ??
'Error fetching data')); // Error state
}

final AnimeModel animeData =
AnimeModel.fromJson(result.data?['Page'] ?? {});
_totalPages = animeData.pageInfo.total;
_animeList.addAll(animeData.media);
emit(AnimeContent(_animeList)); // Success state with anime list
} catch (e) {
emit(AnimeError(e.toString())); // Error state
}
}

Future<void> fetchNextPage() async {
if (_currentPage < _totalPages) {
_currentPage++;
await fetchAnimeData();
}
}
}

Now the cubit utilizes the GraphQLService class to execute GraphQL queries. The core GraphQL query is defined as a static string _animeQuery within the class. The fetchAnimeData method triggers the fetching of anime data based on the GraphQL query, updating the state accordingly—showing loading, handling errors, or displaying the fetched anime content.

The cubit employs pagination to fetch additional pages of anime data through the fetchNextPage method. It keeps track of the current page, total pages, and maintains a list of anime items in the _animeList. The resulting state changes (loading, success, error) enable our Flutter UI to react dynamically to data fetching operations.

Section 3: Bringing It to Life — The Flutter UI

Now that we have our GraphQL integration set up, let’s breathe life into our app with a Flutter UI. The UI is crafted to display the anime list fetched from our GraphQL server. We are going to create a very simple UI since its not the focus of our post.

Setting Up the App Core

// main.dart
void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Anime List',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: BlocProvider(
create: (context) => AnimeCubit(graphQLService: GraphQLService())
..fetchAnimeData(
showLoading: true,
),
child: const MaterialApp(
home: AnimeListPage(),
),
),
);
}
}

In the main.dart file, we've integrated the AnimeCubit into the main application using BlocProvider. This file serves as the entry point to the Flutter application, initiating the AnimeCubit to manage the state and fetching anime data through GraphQL. Additionally, we've set up the theme and color scheme for the application. Stay tuned as we proceed to create the AnimeListPage where we'll showcase the anime list UI.

Anime List Page: Building the Core UI Structure

// anime_list_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class AnimeListPage extends StatelessWidget {
const AnimeListPage({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Anime List'),
),
body: BlocBuilder<AnimeCubit, AnimeState>(
builder: (context, state) {
if (state is AnimeContent) {
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo.metrics.pixels ==
scrollInfo.metrics.maxScrollExtent) {
context.read<AnimeCubit>().fetchNextPage();
}
return false;
},
child: ListView.builder(
itemCount: state.animeList.length,
itemBuilder: (_, index) =>
AnimeItem(anime: state.animeList[index]),
));
} else if (state is AnimeError) {
return Center(
child: Text(state.errorMessage),
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
),
);
}
}

In this section, we introduce the AnimeListPage widget, forming the foundational structure for our anime list app. The widget integrates with the AnimeCubit to handle different states, providing a dynamic interface that responds to loading, error, and content states. Users will witness the basic layout of the app, and we’ll delve into creating the detailed AnimeItem widget in the upcoming steps to enhance the visual presentation of anime entries.

Anime Item Widget: Crafting the Visual Representation

// anime_item.dart
class AnimeItem extends StatelessWidget {
final Media anime;

const AnimeItem({super.key, required this.anime});

@override
Widget build(BuildContext context) {
return Card(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
anime.coverImageUrl ?? '',
width: 128,
),
const SizedBox(
width: 6,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('title(en): ${anime.englishTitle ?? ''}'),
Text('title(original): ${anime.nativeTitle}'),
const SizedBox(
height: 6,
),
Text(
anime.description ?? '',
maxLines: 6,
overflow: TextOverflow.ellipsis,
)
],
),
),
],
),
);
}
}

Here, we present the AnimeItem widget responsible for rendering individual anime entries within our app. This widget showcases a Card-based design, displaying essential details such as cover images, English and native titles, and a brief description.

If we run the app, we would see a screen like this:

Section 4: Handling Diverse Scenarios in Our GraphQL Flutter App

In this concluding section, we explore various scenarios to enhance the functionality and robustness of our GraphQL-powered Flutter app. These insights will empower us to tailor our app to different use cases, ensuring flexibility, security, and reliability in our GraphQL interactions. Whether customizing headers for specialized requirements or making policies, this section equips us with the knowledge to navigate diverse scenarios effectively.

Scenario: Real-time Updates with WebSocketLink and Subscriptions

In this scenario, we enhance our GraphQL-powered Flutter app by introducing real-time updates through WebSocketLink and GraphQL subscriptions. We establish a persistent connection with the server using WebSocketLink, allowing us to receive live updates efficiently. Subscriptions enable us to subscribe to specific events on the server, such as new data or changes, and react to them in real-time.

Implementation:

  • Create a WebSocketLink instance using the WebSocketLink class, specifying the WebSocket endpoint of our GraphQL server.
final _wsLink = WebSocketLink('wss://our-graphql-server/graphql');
  • Split the link route to handle subscriptions separately from other. requests. Combine WebSocketLink with our existing link using Link.split.
link = Link.split(
(request) => request.isSubscription,
_wsLink,
link,
);
  • Define a GraphQL subscription document that corresponds to the events we want to receive updates for. Subscribe to these events using the GraphQL client.
final subscriptionDocument = gql(
r'''
subscription reviewAdded {
reviewAdded {
stars, commentary, episode
}
}
''',
);

subscription = client.subscribe(
SubscriptionOptions(
document: subscriptionDocument,
),
);

subscription.listen(reactToAddedReview);

Implementing WebSocketLink and GraphQL subscriptions enriches our app with real-time features, providing a dynamic and engaging user experience. We can stay updated with live content changes, making our app more interactive and responsive to server-side events.

Scenario: Securing GraphQL Requests with AuthLink

In this scenario, we focus on enhancing the security of our GraphQL requests by implementing AuthLink in our Flutter app. AuthLink allows us to include authentication headers, such as tokens, with each GraphQL request, ensuring that our server can identify and authorize the user. This approach is crucial when dealing with sensitive data or user-specific information.

Implementation:

  • Create an HttpLink instance representing the base URL of our GraphQL server.
final _httpLink = HttpLink(
'https://api.example.com/graphql',
);
  • Instantiate an AuthLink, providing a callback function that retrieves the user’s authentication token. Concatenate AuthLink with the HttpLink to create a combined link.
final _authLink = AuthLink(
getToken: () async => 'Bearer $YOUR_ACCESS_TOKEN',
);

Link _link = _authLink.concat(_httpLink);
  • If WebSocket subscriptions are supported, create a WebSocketLink and split the link route to handle subscriptions separately.
if (websocketEndpoint != null) {
final _wsLink = WebSocketLink(websocketEndpoint);
_link = Link.split(
(request) => request.isSubscription,
_wsLink,
_link,
);
}
  • Initialize the GraphQLClient with the configured link.
final GraphQLClient client = GraphQLClient(
cache: GraphQLCache(),
link: _link,
);

By incorporating AuthLink, our Flutter app now sends authenticated GraphQL requests, providing a secure and authorized communication channel between the client and server. This helps protect user data and ensures that only authorized users can access restricted resources.

Scenario: Configuring Request Policies in GraphQL Client

This scenario explores the flexibility offered by request policies in the GraphQL client for Flutter. Request policies allow us to fine-tune various aspects of the request process, influencing how data is fetched, cached, and handled in response to different scenarios. We can set these policies on any Options object, providing granular control over individual queries, mutations, or subscriptions. Additionally, default policies on the client itself offer a way to establish global behavior for various types of operations.

Implementation:

  • Override Policies for a Single Query:
// override policies for a single query
client.query(QueryOptions(
// return result from network and save to cache.
fetchPolicy: FetchPolicy.networkOnly,
// ignore all GraphQL errors.
errorPolicy: ErrorPolicy.ignore,
// ignore cache data.
cacheRereadPolicy: CacheRereadPolicy.ignore,
// ...
));
  • Set Default Policies on the Client:
GraphQLClient(
defaultPolicies: DefaultPolicies(
// make watched mutations behave like watched queries.
watchMutation: Policies(
FetchPolicy.cacheAndNetwork,
ErrorPolicy.none,
CacheRereadPolicy.mergeOptimistic,
),
),
// ...
)

For comprehensive details on available policies, consult the official GraphQL client documentation. These policies empower developers to tailor the client behavior to specific application needs, ensuring optimized performance and a seamless user experience.

Scenario: Harnessing Code Generation for GraphQL in Flutter

Discover the power of code generation in Flutter, specifically tailored for GraphQL operations. Code generation eliminates the hassle of manual serialization, offering developers confidence through type-safety and enhanced performance. To delve deeper into this impactful feature, explore the graphql_codegen package documentation. Uncover how this tool transforms your development experience, making GraphQL integration in Flutter more robust and efficient.

Conclusion

Congratulations on reaching the end of this journey into building a Flutter app with GraphQL integration! We’ve covered everything from setting up your BLoC architecture to handling GraphQL queries, creating a dynamic UI, and exploring advanced scenarios. Now, it’s your turn! Share your thoughts, experiences, or questions in the comments below. How did you find the process? What challenges did you face?

You can find the project in my github repository.

--

--

Payam Asefi

Senior Flutter Developer with a passion for coding, movies, and nature. Let's connect and code together!