How to Process GraphQL Subscriptions in Flutter

In this blog post, we are going to explain how to work with GraphQL Subscriptions in Flutter. We are going to create a simple counter app that will increase the number by one each time we press a button. We will increase the button with an increment mutation to prevent race conditions between different users. Thus the app will not increment, but the GraphQL endpoint will. After the mutation, we will move on to the subscription to listen to changes. We will provide a GraphQL endpoint so that we can focus solely on the Flutter code.

Setup of the Flutter Project

We are going to use the Graphql Flutter dependency. Installing new dependencies is easy in Flutter. We can simply edit the pubspec.yml. Here we will add the dependency!

dependencies:
  graphql_flutter: ^3.0.0

Don’t forget to run flutter pub get, before you continue!

Performing a Mutation on a GraphQL Endpoint

The mutation we want to perform is to increase the current number by one. We want to be able to do this without knowing the actual number, as another user might have already increased the number. Luckily Hasura supports an increment operation that does exactly this. In their post they give the following mutation as an example, which we can rewrite for our use case:

mutation IncrementCounter {
  update_counter(
    where: {id: {_eq: 1}},
    _inc: {count: 1}
  ) {
    affected_rows
    returning {
      id
      count
    }
  }
}

We have to make some small changes to the parent Widget before we can start with the mutation. We are going to wrap the widgets with a GraphQLProvider. This will make we can use the Mutation of the Flutter GraphQL project. For this, we have to provide a link to the GraphQL Endpoint. We are going to use an endpoint we have created at Hasura, https://counter.hasura.app/v1/graphql.

class GraphQLSubscriptionDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final HttpLink link = HttpLink(
      uri: 'https://counter.hasura.app/v1/graphql',
    );

    ValueNotifier<GraphQLClient> client = ValueNotifier(
      GraphQLClient(
        cache: InMemoryCache(),
        link: link,
      ),
    );

    return GraphQLProvider(
      client: client,
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.green,
            title: Text("Graphql Subscription Demo"),
          ),
          body: Padding(
            padding: const EdgeInsets.all(20.0),
            child: Column(
              children: [
                IncrementButton(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Now we can add the IncrementButton. This will be a simple button, that will run the mutation. For this, we can use the Mutation Widget. Here we provide a builder that returns a button. The builder has a runMutation option that we can execute when the button is pressed. As we always update the counter with id 1, we do not have to provide any options for the mutation. The mutation is provided as a string value to the mutation through the options. This is all we have to do to increment the count!

class IncrementButton extends StatelessWidget {
  static String incr = '''mutation IncrementCounter {
  update_counter(
    where: {id: {_eq: 1}},
    _inc: {count: 1}
  ) {
    affected_rows
    returning {
      id
      count
    }
  }
}''';

  @override
  Widget build(BuildContext context) {
    return Mutation(
      options: MutationOptions(
        documentNode: gql(incr),
      ),
      builder: (
        RunMutation runMutation,
        QueryResult result,
      ) {
        return Center(
          child: RaisedButton.icon(
            onPressed: () {
              runMutation({});
            },
            icon: Icon(Icons.plus_one),
            label: Text(""),
          ),
        );
      },
    );
  }
}

Listening to a GraphQL Subscription

Now that we can update the count with our app, it is time for the fun part! Listening to updates of the count in another Widget. This Widget will display the current value of the counter and will update when new events are published on the subscription. First, let’s start with how the subscription looks like!

subscription WatchCounter {
  counter(where: {id: {_eq: 1}}) {
    count
  }
}

We have to make some small changes to the parent Widget before we can start with the subscription. For the mutation, we only provided the HTTP link to the GraphQL endpoint. For subscriptions, we also have to add a Websocket Link, so that we can open a connection through which the server can publish the updates. We will add the Websocket link and combine it with the HTTP link.

    final HttpLink httpLink = HttpLink(
      uri: 'https://counter.hasura.app/v1/graphql',
    );

    final WebSocketLink websocketLink = WebSocketLink(
      url: 'wss://counter.hasura.app/v1/graphql',
      config: SocketClientConfig(
        autoReconnect: true,
        inactivityTimeout: Duration(seconds: 30),
      ),
    );

    ValueNotifier<GraphQLClient> client = ValueNotifier(
      GraphQLClient(
        cache: InMemoryCache(),
        link: httpLink.concat(websocketLink),
      ),
    );

We can use this subscription in almost precisely the same way as we do with the mutation. We are going to create a new StatelessWidget. In the build method, we will return the Subscription Widget. Here we have to provide the subscription as a string, just like the mutation. We also have to provide the name of the subscription, WatchCounter. The most important part, the builder will return the state of the subscription. If the subscription is not loading and we have no errors, we will return the value of the counter!

class Counter extends StatelessWidget {
  static String subscription = '''subscription WatchCounter {
  counter(where: {id: {_eq: 1}}) {
    count
  }
}''';

  @override
  Widget build(BuildContext context) {
    return Subscription(
      "WatchCounter",
      subscription,
      builder: ({
        bool loading,
        dynamic payload,
        dynamic error,
      }) {
        if (loading == false && error == null) {
          return Text(payload['counter'][0]['count'].toString());
        } else {
          return Text("Fetching Count");
        }
      },
    );
  }
}

If you are interested in the full code, it can be found here on Github. If you are using more complicated models, we would suggest generating models with Artemis for your queries, mutations, and subscriptions. In this blog post, we show how to generate the models for the queries of your Flutter GraphQL endpoint. Thank you for reading, and if you have any questions, remarks or suggestions feel free to leave a comment!

Leave a Reply