Mutations on a GraphQL Endpoint with Flutter

In the previous blog post, we described how to query a GraphQL endpoint. Here we retrieved the information about Formula 1 standings. Of course, there are more races in this season, and (hopefully) this is not the last season. So in this blog post, we will describe how to execute mutations. This way we can insert new races that are coming!

Setup of the 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! We are also adding another dependency for a simple date time picker since we are going to insert races. Those races also need a date and time.

dependencies:
  graphql_flutter: ^3.0.0
  datetime_picker_formfield: ^1.0.0

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

Creating a new race

Before we can insert the results of a race, we first need the race itself. In the schema, the race currently needs a reference to the season. Furthermore, we want a name, data, and round of the race. For this we can use the following GraphQL mutation:

mutation InsertRace($date: date, $name: String, $season_id: uuid, $round: Int) {
  insert_Race(objects: [{date: $date, name: $name, season_id: $season_id, round: $round}]) {
    returning {
      name
    }
  }
}

Before we can execute this mutation, we have to initialize the client. We do this in a new Class. Here we provide a GraphQLClient with a link to the GraphQL API. Now we can retrieve the client from the configuration to execute the mutation.

class GraphQLConfiguration {
  static HttpLink httpLink = HttpLink(
    uri: 'https://brief-quagga-80.hasura.app/v1/graphql',
  );
  ValueNotifier<GraphQLClient> client = ValueNotifier(
    GraphQLClient(
      link: httpLink,
      cache: OptimisticCache(dataIdFromObject: typenameDataIdFromObject),
    ),
  );
  GraphQLClient clientToQuery() {
    return GraphQLClient(
      cache: OptimisticCache(dataIdFromObject: typenameDataIdFromObject),
      link: httpLink,
    );
  }
}

We are going to create a simple form so that we can enter the required field for a new race. These are the date, name, and round. For the date, we have also installed the datetime_picker_formfield. This allows us to define a simple date picker. For the round and the name, we will use a simple TextFormField. We map each of them to the corresponding values, so that when the user press save we can perform the mutation.

class _MutationTriggerState extends State<MutationTrigger> {
  String name;
  int round;
  DateTime raceDate;
  @override
  Widget build(BuildContext context) {
    final formKey = GlobalKey<FormState>();
    return Form(
      key: formKey,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          new TextFormField(
            decoration: const InputDecoration(
              hintText: 'Enter the race name',
              labelText: 'Name',
            ),
            validator: (value) {
              if (value.isEmpty) {
                return 'Please enter the name';
              }
              return null;
            },
            onSaved: (value) => name = value,
          ),
          SizedBox(
            height: 10,
          ),
          new TextFormField(
            onSaved: (value) => round = int.parse(value),
            decoration: const InputDecoration(
              hintText: 'Enter the race round',
              labelText: 'Round',
            ),
            validator: (value) {
              if (value.isEmpty) {
                return 'Please enter a round';
              }
              return null;
            },
            keyboardType: TextInputType.number,
            inputFormatters: [
              FilteringTextInputFormatter.digitsOnly,
            ],
          ),
          SizedBox(
            height: 10,
          ),
          BasicDateTimeField(
            saveDate: (dateTime) => raceDate = dateTime,
          ),
          RaisedButton(
            child: Text('Save'),
            onPressed: (() async {
              if (formKey.currentState.validate()) {
//todo mutate
              }
            }),
          ),
        ],
      ),
    );
  }
}

Now that we have a simple form in which we can access the variables needed for the mutation, it is time to execute the mutation. For this, we only have to change the action that is performed after pressing the button. Here we are using the client that we initialized earlier and provided with the GraphQLConfiguration to this Widget. We will define the mutation as a string and provide it to the client. The variables will be provided at the document options.

            onPressed: (() async {
              if (formKey.currentState.validate()) {
                formKey.currentState.save();
                graphQLConfiguration.client.mutate(MutationOptions(
                  documentNode: gql(_MutationTriggerState.insertRace),
                  variables: {
                    'date': DateFormat("yyyy-MM-ddTHH:mm:ss").format(raceDate),
                    'name': name,
                    'round': round,
                    'season_id': 'f8e7d546-7556-4ccf-9615-7186c7d6a113'
                  },
                ));
              }

We have now executed our first mutation! The full code can be found here on Github. One of the things that I disliked, coming from more typed based languages (Kotlin, Java) was the way we had to convert the Dart DateTime to a different format before we could insert it. Luckily this can be solved. In the previous blog post, we already described how to generate models for queries. In the next blog post, we will describe typing our model based on the schema of the mutation. Here we have to provide a different mapping for two fields that cannot be mapped out of the box. Thank you for reading and do not hesitate to ask questions.

Leave a Reply