Search code examples
apifluttergraphqlhttp-post

How to send post request to graphql API in flutter


I'm trying to learn how to use rails combined with graphql to create a rails API by developing a simple app that just retrieves text (in my case, quotes) from a database and shows it on screen. I am using flutter for frontend and rails with graphql as the backend. The backend part was easy to create because I already had some rails knowledge but the frontend part is something I'm new to and I'm trying to figure out how to access a graphql query that I created via flutter to get the data that needs to be displayed.

Below is the flutter code that I currently have (partially adapted from How to build a mobile app from scratch with Flutter and maybe Rails?).

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<Quote> fetchQuote() async {
  final response =
      await http.get('http://10.0.2.2:3000/graphql?query={quote{text}}');

  if (response.statusCode == 200) {
    // If the call to the server was successful, parse the JSON.
    return Quote.fromJson(json.decode(response.body));
  } else {
    // If that call was not successful, throw an error.
    throw Exception('Failed to load quote');
  }
}

class Quote {
  final String text;

  Quote({this.text});

  factory Quote.fromJson(Map<String, dynamic> json) {
    return Quote(
      text: json['text']
    );
  }
}


void main() => runApp(MyApp(quote: fetchQuote()));

class MyApp extends StatelessWidget {
  final Future<Quote> quote;

  MyApp({this.quote});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fetch Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Fetch Data Example'),
        ),
        body: Center(
          child: FutureBuilder<Quote>(
            future: quote,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(snapshot.data.text);
              } else if (snapshot.hasError) {
                return Text("${snapshot.error}");
              }

              // By default, show a loading spinner.
              return CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}

Some obvious reasons why this code is wrong that I already figured out myself is that the graphql server expects a post request for the query while my code is sending a get request but that is my question. How do I send a post request for my graphql server in flutter to retrieve the data? The query that I'm trying to access is the one after '?query=' in my flutter code.


Solution

  • This took me a minute to figure out, too, but here is what I did in my practice todo app:

    1 - Read this page on graphql post requests over http. There is a section for GET Requests as well as POST.

    2 - Make sure your body function argument is correctly json-encoded (see code below).

    Tip: Using Postman, you can test the graphql endpoint w/different headers & authorization tokens, and request bodies. It also has a neat feature to generate code from the request. Check out this page for details. It's not 100% accurate, but that's what helped me figure out how to properly format the request body. In the function post, apparently you can't change the content-type if you provide a Map as the body of the request (and the request content types is application/json), so a String worked for my use case.

    Sample Code (uses a GqlParser class to properly encode the request body):

    import 'dart:convert';
    import 'package:http/http.dart' as http;
    import 'todo.dart';
    import '../creds/creds.dart';
    import 'gql_parser.dart';
    
    const parser = GqlParser('bin/graphql');
    
    class TodoApiException implements Exception {
      const TodoApiException(this.message);
      final String message;
    }
    
    class TodoApiClient {
      const TodoApiClient();
      static final gqlUrl = Uri.parse(Credential.gqlEndpoint);
      static final headers = {
        "x-hasura-admin-secret": Credential.gqlAdminSecret,
        "Content-Type": "application/json",
      };
    
      Future<List<Todo>> getTodoList(int userId) async {
        final response = await http.post(
          gqlUrl,
          headers: headers,
          body: parser.gqlRequestBody('users_todos', {'userId': userId}),
        );
    
        if (response.statusCode != 200) {
          throw TodoApiException('Error fetching todos for User ID $userId');
        }
    
        final decodedJson = jsonDecode(response.body)['data']['todos'] as List;
        var todos = <Todo>[];
    
        decodedJson.forEach((todo) => todos.add(Todo.fromJson(todo)));
        return todos;
      }
    // ... rest of class code ommitted
    

    Per the .post() body argument documentation:

    If it's a String, it's encoded using [encoding] and used as the body of the request. The content-type of the request will default to "text/plain".

    If [body] is a List, it's used as a list of bytes for the body of the request.

    If [body] is a Map, it's encoded as form fields using [encoding]. The content-type of the request will be set to "application/x-www-form-urlencoded"; this cannot be overridden.

    I simplified the creation of a string to provide as the body of an argument with the following code below, in a GqlParser class. This will allow you to have a folder such as graphql that contains multiple *.graphql queries/mutations. Then you simply use the parser in your other classes that need to make simple graphql endpoint requests, and provide the name of the file (without the extension).

    import 'dart:convert';
    import 'dart:io';
    
    class GqlParser {
      /// provide the path relative to of the folder containing graphql queries, with no trailing or leading "/".
      /// For example, if entire project is inside the `my_app` folder, and graphql queries are inside `bin/graphql`,
      /// use `bin/graphql` as the argument.
      const GqlParser(this.gqlFolderPath);
    
      final String gqlFolderPath;
    
      /// Provided the name of the file w/out extension, will return a string of the file contents
      String gqlToString(String fileName) {
        final pathToFile =
            '${Directory.current.path}/${gqlFolderPath}/${fileName}.graphql';
        final gqlFileText = File(pathToFile).readAsLinesSync().join();
        return gqlFileText;
      }
    
      /// Return a json-encoded string of the request body for a graphql request, given the filename (without extension)
      String gqlRequestBody(String gqlFileName, Map<String, dynamic> variables) {
        final body = {
          "query": this.gqlToString(gqlFileName),
          "variables": variables
        };
        return jsonEncode(body);
      }
    }