Powering Flutter Apps with GraphQL: Unleashing Efficient Data Queries
Amir Hosein Asefi Nejad
Lead Flutter Engineer at On | Enthusiastic about Building User-Friendly Mobile Experiences & Continuous Learning
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:
Key Differences from REST API:
Understanding Queries and Mutations:
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:
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:
final _wsLink = WebSocketLink('wss://our-graphql-server/graphql');
link = Link.split(
(request) => request.isSubscription,
_wsLink,
link,
);
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:
final _httpLink = HttpLink(
'https://api.example.com/graphql',
);
final _authLink = AuthLink(
getToken: () async => 'Bearer $YOUR_ACCESS_TOKEN',
);
Link _link = _authLink.concat(_httpLink);
if (websocketEndpoint != null) {
final _wsLink = WebSocketLink(websocketEndpoint);
_link = Link.split(
(request) => request.isSubscription,
_wsLink,
_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
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,
// ...
));
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.
Senior Mobile Application Developer
9 个月great article
Product Manager open for a new Position
9 个月Just last Friday you summarised GraphQL for me in a couple of sentences in a very simple way and now I can read up on this in detail. Really interesting to learn about this technology. Thanks so much for this comprehensive article! Well done!
Dart and Flutter Developer
9 个月??
Software Engineer | Flutter at On
9 个月awesome article. ??