Consuming GraphQL APIs with Flutter
Tags: graphql, flutter
Published: 2023-04-28 23:15:58

by [Joakim Gakure](https://blog.openreplay.com/authors/joakim-gakure)

<blockquote><em>
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.</em></blockquote>



You have your [Flutter SDK](https://docs.flutter.dev/get-started/install) ready for building Flutter apps. To consume GraphQL APIs using Flutter, you need a GraphQL API to [query](https://graphql.org/learn/queries/) 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](https://graphqltodosapi-production.up.railway.app/).

Additionally, Flutter cannot directly understand data from a GraphQL API. You need a [graphql_flutter](https://pub.dev/packages/graphql_flutter) package. It provides a set of high-level [widgets](https://docs.flutter.dev/development/ui/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:

```bash
flutter create flutter_graphql
```

Access the newly created directory:

```bash
cd flutter_graphql
```

[Install graphql_flutter](https://pub.dev/packages/provider/install) for interacting with the GraphQL API using the Flutter pub package manager:

```bash
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:

```dart
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.

```dart
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](https://pub.dev/packages/graphql_flutter) 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](https://docs.flutter.dev/development/ui/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](https://graphqltodosapi-production.up.railway.app/), you need to create a schema. A [schema](https://graphql.org/learn/schema/) is a blueprint of data types that can be queried from GraphQL API operations. To create one, you use the [GraphQL schema language](https://graphql.org/learn/schema/#type-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](https://graphqltodosapi-production.up.railway.app/) uses a todos data as it can be tested from the [GraphQL Playground](https://graphqltodosapi-production.up.railway.app/graphiql).

![-](https://blog.openreplay.com/images/consuming-graphql-apis-with-flutter/images/VWlwEWJ.png)

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:

```dart
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](https://graphql.org/learn/queries/#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.

```dart
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.

```dart
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](https://pub.dev/packages/provider). It’s a Flutter package for managing your app's state and dependencies.

```bash
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:

```dart
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:

```dart
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:

```dart
 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:

```dart
void clearResponse() {
    _response = '';
    notifyListeners();
}
```

<div style="background-color:#efefef; border-radius:8px; padding:10px; display:block;">
<hr/>
<h3><em>Session Replay for Developers</em></h3> 
<p><em>Uncover frustrations, understand bugs and fix slowdowns like never before with <strong><a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a></strong> — an open-source session replay suite for developers. It can be <strong>self-hosted</strong> in minutes, giving you complete control over your customer data.</em></p>
<img alt="OpenReplay" style="margin-top:5px; margin-bottom:5px;" width="768" height="400" src="https://raw.githubusercontent.com/openreplay/openreplay/main/static/openreplay-git-hero.svg" class="astro-UXNKDZ4E" loading="lazy" decoding="async">
<p><em>Happy debugging! <a href="https://openreplay.com" target="_blank">Try using OpenReplay today.</a></em><p>
<hr/>
</div>


## 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:

```dart
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:

```dart
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:

```dart
void clearResponse() {
    _response = '';
    notifyListeners();
}
```

Correspondingly, for toggle mutation, add a `toggle_todo_provider.dart` file and create `ToggleTodoProvider` to execute a `toggleTodo` method.

```dart
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:

```dart
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:

```dart
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:

```dart
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:

```dart
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:

```dart
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:

```dart
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:

```dart
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:

```dart
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:

```dart

class AddTodo extends StatefulWidget {
    const AddTodo({Key? key}) : super(key: key);

    @override
    _AddTodoState createState() => _AddTodoState();
}
```

Proceed and create the `_AddTodoState` class as follows:

```dart

class _AddTodoState extends State<AddTodo> {
    final TextEditingController _todo = TextEditingController();
    // Build context
```

In the above `_AddTodoState` class, create its build and a `Scaffold` as follows:

```dart
@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:

```dart

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:

```dart
Container(
    margin: const EdgeInsets.all(20),
    child: const Text('Add your first todo'),
),
```

- Add the fields for text inputs:
  
```dart
//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 the `addTodo()` method to save the added todo data:

```dart
//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.

```dart
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:

```bash
flutter run
```

Once the application start, the fetched GraphQL data will be displayed on the home screen.

![-](https://blog.openreplay.com/images/consuming-graphql-apis-with-flutter/images/YXTlKH5.jpg)

Clicking the plus button will launch the `add todo screen` to execute the mutations for adding data.

![-](https://blog.openreplay.com/images/consuming-graphql-apis-with-flutter/images/cbq5skt.jpg)

## 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.

<Endcta />