Flutter is a popular open-source framework for developing mobile, desktop, and web applications, and your Flutter apps will need to consume data from a server. This article will teach you how to approach this problem with GraphQL APIs.
You have your Flutter SDK ready for building Flutter apps. To consume GraphQL APIs using Flutter, you need a GraphQL API to query and retrieve data from. This can be either a public API or an API you build yourself. Along this guide, you will use data from this GraphQL API.
Additionally, Flutter cannot directly understand data from a GraphQL API. You need a graphql_flutter package. It provides a set of high-level widgets for using GraphQL in your Flutter app. You need to install it using the pub package manager. But first, proceed to your preferred working directory and run the following command to initialize the project:
flutter create flutter_graphql
Access the newly created directory:
cd flutter_graphql
Install graphql_flutter for interacting with the GraphQL API using the Flutter pub package manager:
flutter pub get provider graphql_flutter
Flutter is the client application. It needs a connection to the server that serves the GraphQL API. With that, create a utils
directory in the lib
directory to hold an url.dart
that will host the server endpoint that the client will call:
const String endpointUrl =
"https://graphqltodosapi-production.up.railway.app/";
Still, on the same, add an Endpoint.dart
to host the definition of the GraphQL client instance.
import 'package:flutter/foundation.dart';
import 'package:graphql_app/Utils/url.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
class EndPoint {
ValueNotifier<GraphQLClient> getClient() {
ValueNotifier<GraphQLClient> _client = ValueNotifier(GraphQLClient(
link: HttpLink(endpointUrl, defaultHeaders: {
}),
cache: GraphQLCache(store: HiveStore()),
));
return _client;
}
}
The defined Endpoint
class holds an instance of GraphQLClient that links your Flutter app and a GraphQL API. Note that the GraphQLClient
is wrapped in a ValueNotifier
and returned by the getClient()
method. The ValueNotifier
will notify listeners of changes to the GraphQLClient
instance. This way, it can be used by the Flutter widgets throughout your Flutter app and updated dynamically based on every request to the API.
Setting up the schemas
For Flutter to understand the GraphQL API, you need to create a schema. A schema is a blueprint of data types that can be queried from GraphQL API operations. To create one, you use the GraphQL schema language. It allows you to validate client queries before executing them on the API. This reduces data errors between the client and the server. The client can only request data that is defined in the schema.
The example GraphQL API uses a todos data as it can be tested from the GraphQL Playground.
Therefore, a schema is needed to define the data structure, data types, and the fields available for each type. To do exactly that, create the Schemas
directory in the lib
folder of your Flutter project. This will be used to host the schema files to perform the GraphQL operations.
The first operation this guide will demonstrate is performing the GraphQL queries. This operation allows you to fetch data from a GraphQL API, an equivalent of GET from REST APIs. Add a get_todo_schema.dart
file in the Schemas
directory and create a GetTodoSchema()
schema as a blueprint for fetching data using the query
type as follows:
class GetTodoSchema {
static String getTodoJson = """
query getTodos{
todos{
id
title
completed
}
}
""";
}
Adding data involves sending POST requites in REST APIs. Nevertheless, in GraphQL, you create mutations. Mutations are executed on the server to allow clients to make changes to the data stored on the server. You modify data on a server using mutations. Mutations are defined in the GraphQL schema as fields using a Mutation
type. To create data, add a new add_todo_schema.dart
file with a mutation that will be called while creating todo.
class AddTodoSchema {
static String addTodoJson = """
mutation addTodo(\$title:String!){
add(title:\$title){
id
title
completed
}
}
""";
}
You can create unlimited mutations. They modify data. Thus they keep track of create, update, and delete. Below is an additional mutation that updated data in a GraphQL API. It changes the completed field of a todo from true to false and vice versa. Add a toggle_todo_schema.dart
file with the following ToggleTodoSchema
mutation.
class ToggleTodoSchema {
static String toggleTodoJson = """
mutation updateTodo(\$id:String!){
toggle(id:\$id){
id
title
completed
}
}
""";
}
Creating GraphQL queries with Flutter
To perform this operation, it’s good to use a state management approach to manage data, as it will be shared between multiple widgets in your app. In this demonstration, you will use a provider. It’s a Flutter package for managing your app’s state and dependencies.
flutter pub add provider
Provider provides a simple and efficient way to access and manage data that need to be passed down the widget tree in a Flutter app.
Provider will return the GraphQLClient
instance from the EndPoint
class. This makes the GraphQLClient
instance available to all widgets in the widget tree.
To create a provider for consuming the GraphQL queries, create a Providers
directory in the lib
folder. A get_todos_provider.dart
file will hold the providers for getting data here. It will define a GetTodoProvider
that will contain the following instance variables:
import 'package:flutter/foundation.dart';
import 'package:graphql_app/Schemas/get_todo_schema.dart';
import 'package:graphql_app/Utils/Endpoint.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
class GetTodoProvider extends ChangeNotifier {
bool _status = false;
String _response = '';
dynamic _list = [];
bool get getStatus => _status;
String get getResponse => _response;
final EndPoint _point = EndPoint();
}
The GetTodoProvider
class will define a getTodo
function for fetching the todos as defined in the GraphQLClient
notifier:
void getTodo(bool isLocal) async {
ValueNotifier<GraphQLClient> _client = _point.getClient();
QueryResult result = await _client.value.query(
QueryOptions(
document: gql(GetTodoSchema.getTodoJson),
fetchPolicy: isLocal == true ? null : FetchPolicy.networkOnly,
),
);
if (result.hasException) {
// check if we have any exception
_status = false;
if (result.exception!.graphqlErrors.isEmpty) {
_response = "Internet is not found";
} else {
_response = result.exception!.graphqlErrors[0].message.toString();
}
notifyListeners();
} else {
// no exception, set the todos (list)
_status = false;
_list = result.data;
notifyListeners();
}
}
GetTodoProvider
should then return the todos data list:
dynamic getTodosData() {
if (_list.isNotEmpty) {
final data = _list;
return data['todos'] ?? {};
} else {
return {};
}
}
Since you will display the stored responses to the user using the Flutter client, the initial sent response can be cleared. Create a method for clearing the response:
void clearResponse() {
_response = '';
notifyListeners();
}
Consuming mutations with Flutter
To the consumer, the mutations create a provider, using GetTodoProvider
as the example. For adding data, mutation creates an add_todo_provider.dart
file that defines a method for adding a todo using AddTodoProvider
as follows:
import 'package:flutter/foundation.dart';
import 'package:graphql_app/Schemas/add_todo_schema.dart';
import 'package:graphql_app/Utils/Endpoint.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
class AddTodoProvider extends ChangeNotifier {
bool _status = false;
String _response = '';
bool get getStatus => _status;
String get getResponse => _response;
final EndPoint _point = EndPoint();
}
Inside the AddTodoProvider
class, define a method for adding a todo, addTodo()
as follows:
void addTodo({String? todo}) async {
_status = true;
_response = "Please wait...";
notifyListeners();
ValueNotifier<GraphQLClient> _client = _point.getClient();
QueryResult result = await _client.value.mutate(
MutationOptions(document: gql(AddTodoSchema.addTodoJson), variables: {
'title': todo,
}));
if (result.hasException) {
// Check if there is any exception raised.
_status = false;
if (result.exception!.graphqlErrors.isEmpty) {
_response = "Internet is not found";
} else {
_response = result.exception!.graphqlErrors[0].message.toString();
}
notifyListeners();
} else {
_status = false;
_response = "Todo was successfully added";
notifyListeners();
}
}
Inside the AddTodoProvider
class, add a function for clearing the response:
void clearResponse() {
_response = '';
notifyListeners();
}
Correspondingly, for toggle mutation, add a toggle_todo_provider.dart
file and create ToggleTodoProvider
to execute a toggleTodo
method.
import 'package:flutter/foundation.dart';
import 'package:graphql_app/Schemas/toggle_todo_schema.dart';
import 'package:graphql_app/Utils/Endpoint.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
class ToggleTodoProvider extends ChangeNotifier {
bool _status = false;
String _response = '';
bool get getStatus => _status;
String get getResponse => _response;
final EndPoint _point = EndPoint();
}
The toggleTodo
method will execute the provider as follows:
void toggleTodo({ String? todoId,}) async {
_status = true;
_response = "Please wait...";
notifyListeners();
ValueNotifier<GraphQLClient> _client = _point.getClient();
QueryResult result = await _client.value.mutate(
MutationOptions(document: gql(ToggleTodoSchema.toggleTodoJson), variables: {
'id': todoId,
}));
if (result.hasException) {
// Check if there is an exception
_status = false;
if (result.exception!.graphqlErrors.isEmpty) {
_response = "Internet is not found";
} else {
_response = result.exception!.graphqlErrors[0].message.toString();
}
notifyListeners();
} else {
_status = false;
_response = "Todo was successfully Toggled";
notifyListeners();
}
}
Create a method for clearing its response:
void clearResponse() {
_response = '';
notifyListeners();
}
Fetching and displaying data
You have the GraphQL data ready. Using Flutter widgets, pass the data to the widgets tree so Flutter can interact with the API.
The following widget will display the GraphQL data query request. Create the Screens
directory in the lib
directory. A home_page.dart
file will execute the stateful widget HomePage
for displaying the todos.
First, we will import the packages needed to fetch data to the home_page.dart
file:
import 'package:flutter/material.dart';
import 'package:graphql_app/Providers/toggle_todo_provider.dart';
import 'package:graphql_app/Providers/get_todo_provider.dart';
import 'package:graphql_app/Screens/add_todo.dart';
import 'package:provider/provider.dart';
Add the following HomePage
StatefulWidget
and a build method for executing a Scaffold
that defines the Flutter widgets:
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool isFetched = false;
@override
Widget build(BuildContext context) {
return Scaffold(
// We will add the Scaffold code here
);
}
}
In the above Scaffold
, we will create an AppBar
, a body
containing a CustomScrollView
, a floatingActionButton
for adding new Todos, and a widget for executing the GetTodoProvider
as follows:
return Scaffold(
appBar: AppBar(
title: const Text('Todos'),
),
body: Consumer<GetTodoProvider>(builder: (context, todo, child) {
// Fetch todos inside the CustomScrollView
}),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const AddTodo()));
},
label: Icon(
Icons.add,
),
),
);
GetTodoProvider
will fetch and map the todos to the CustomScrollView
. This will be executed by the body
. Therefore, we will implement the code for fetching todos inside the CustomScrollView
as follows:
if (isFetched == false) {
// fetch the data
todo.getTodo(true);
Future.delayed(const Duration(seconds: 3), () => isFetched = true);
}
return RefreshIndicator(
onRefresh: () {
todo.getTodo(false);
return Future.delayed(const Duration(seconds: 3));
},
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
// Container
),
)
],
),
);
The above Container
will hold a ListView
for displaying the data. The Container will check if getTodoData
has any data response (getResponseData
) and display it. The ListTile
will show the data to the screen containing a title and toggle todo between completed and in progress as follows:
height: MediaQuery.of(context).size.height,
padding: const EdgeInsets.all(20),
child: Column(
children: [
Container(
margin: const EdgeInsets.all(20),
child: const Text("Added Todos"),
),
if (todo.getTodoData().isEmpty)
const Text('No Todo found'),
Expanded(
child: ListView(
children: List.generate(todo.getResponseData().length,
(index) {
final data = todo.getResponseData()[index];
return ListTile(
contentPadding: const EdgeInsets.all(0),
title: Text(data['title']),
subtitle: data['completed'] ? Text("Completed") : Text("Not Completed"),
leading: CircleAvatar(
backgroundColor: Colors.grey,
child: Text((index + 1).toString()),
),
trailing: Consumer<ToggleTodoProvider>(
builder: (context, toggle, child) {
WidgetsBinding.instance!
.addPostFrameCallback((_) {
if (toggle.getResponse != '') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(toggle.getResponse)));
toggle.clear();
}
});
return Row(
mainAxisSize:MainAxisSize.min,
children:[
IconButton(
onPressed: () {
//Toggle todo
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(
content: const Text(
"Are you sure you want to toggle todo?"),
action: SnackBarAction(
label: "Toggle",
onPressed: () {
toggle.toggleTodo(
todoId: data['id']);
}),
));
},
icon: const Icon(Icons.edit)),
]
);
}),
);
}),
)),
const SizedBox(height: 150),
],
),
Equivalently, create an add_todo.dart
file for hosting the functionality of adding a todo by defining an AddTodo
stateful widget to handle adding a todo. First, we will add import statements needed to add data to the add_todo.dart
file:
Import statements:
import 'package:flutter/material.dart';
import 'package:graphql_app/Providers/add_todo_provider.dart';
import 'package:provider/provider.dart';
Create a AddTodo
for _AddTodoState
definition:
class AddTodo extends StatefulWidget {
const AddTodo({Key? key}) : super(key: key);
@override
_AddTodoState createState() => _AddTodoState();
}
Proceed and create the _AddTodoState
class as follows:
class _AddTodoState extends State<AddTodo> {
final TextEditingController _todo = TextEditingController();
// Build context
In the above _AddTodoState
class, create its build and a Scaffold
as follows:
@override
Widget build(BuildContext context) {
return Scaffold(
// We will create the Scaffold here
)
)
With the Scaffold ready, we can add its AppBar
, and body
containing the CustomScrollView
as follows:
appBar: AppBar(
title: const Text('Add New Todo'),
),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.all(10),
child: Column(
children: [
// Add the Container Columns
],
),
),
)
],
),
The body
will contain the following children that we will use for adding data.
- First, add the title for the Add Todos screen:
Container(
margin: const EdgeInsets.all(20),
child: const Text('Add your first todo'),
),
- Add the fields for text inputs:
//Add todo input field
TextFormField(
controller: _todo,
decoration: const InputDecoration(
labelText: 'Todo',
),
),
- Create a
Save Todo
button that will take the text input payload and execute theaddTodo()
method to save the added todo data:
//Add todo button
Consumer<AddTodoProvider>(builder: (context, todo, child){
WidgetsBinding.instance!.addPostFrameCallback((_) {
if (todo.getResponse != '') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(todo.getResponse)));
todo.clear();
}
});
return GestureDetector(
onTap: todo.getStatus == true
? null
: () {
//Add todo button
if (_todo.text.isNotEmpty) {
todo.addTodo(
todo: _todo.text.trim());
}
},
child: Container(
padding: const EdgeInsets.all(15),
margin: const EdgeInsets.all(30),
decoration: BoxDecoration(
color: todo.getStatus == true
? Colors.grey
: Colors.green,
borderRadius: BorderRadius.circular(10)),
child: Text(todo.getStatus == true
? 'Loading...'
: 'Save Todo')),
);
})
Wrapping up
On your lib/main.dart
, perform the following updates to ensure the providers, screens, and GraphQL Flutter has been executed.
import 'package:flutter/material.dart';
import 'package:graphql_app/Providers/toggle_todo_provider.dart';
import 'package:graphql_app/Providers/get_todo_provider.dart';
import 'package:graphql_app/Providers/add_todo_provider.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:provider/provider.dart';
import 'Screens/home_page.dart';
void main() async {
await initHiveForFlutter();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AddTodoProvider()),
ChangeNotifierProvider(create: (_) => GetTodoProvider()),
ChangeNotifierProvider(create: (_) => ToggleTodoProvider())
],
child: MaterialApp(
home: HomePage(),
theme: ThemeData(
primaryColor: Colors.green,
primarySwatch: Colors.green
)
));
}
}
Your application is ready. You can test it using the following command that should be executed in your project directory:
flutter run
Once the application start, the fetched GraphQL data will be displayed on the home screen.
Clicking the plus button will launch the add todo screen
to execute the mutations for adding data.
Conclusion
GraphQL and Flutter provide an easy approach to building mobile apps that can handle data retrieval and manipulation. GraphQL ensures you deliver optimal user experience by ensuring Flutter gets the exactly required data by reducing the rounds of requests between server and client.