Using GraphQL in Android 🚀

One of the first things one comes across when dealing with GraphQL for mobile applications is the Apollo GraphQL library by Meteor Development Group Inc. The Apollo Client provides an easy-to-use client for a GraphQL endpoint and is available for most platforms, including Angular, iOS and Android.

While you can choose Apollo Android to use as the GraphQL client for the Yoli API, we advise you to evaluate your choice regarding the pitfalls we encountered ourselves.

The Problem

Getting started with a GraphQL endpoint is made easy with Apollo Android, as the data model is automatically generated by parsing the GraphQL schema and the mapping from the raw data to this model happens automatically.

However, after taking our first steps with GraphQL and Apollo, we encountered a major drawback: The models are generated query-wise (mutation-wise, respectively) and thus, different requests that return the same type of data get different models. That means, you cannot easily merge results from those requests to display them in the UI, or forward the results from one requests into the parameters of another request.

An example.

{
    transActionQuery {      
        id
        partner
        amount
    }
}

{
    userQuery(userId: "1") {        
        transactions {
            id
            partner
            amount
        }
    }
}

These are two possible queries for a GraphQL endpoint like the Yoli API. It is easy to see that both will return transaction data, containing the id, the partner and the amount of each transaction. What we would like to have is that Apollo generates a Transaction model for us and maps the results of these queries to a list of those.

Unfortunately, as the model generation is done from the static GraphQL schema file, Apollo generates a Transaction model and a UserTransaction model (naming may vary) from these queries. Now we want efficient code and follow the 'DRY' principle, so we would implement one screen to display a list of transactions, no matter which request provides them. But as the generated model cannot inherit from an interface or superclass that our screen controller could handle, we would have to implement it two times: once for Transaction and once for UserTransaction. At least, we would have to handle two separate collections in our controller. Overall, this behavior would create a lot of overhead to maintain over time.

This is only a simple example, but the problem should be illustrated by it.

The Solution

Of course you could get around these problems by mapping to a custom, homogenic data model. But this would cause an overhead in casting back and forth between Apollos generated model and your internal model and maintaining both models as the application and the schema evolves.

So instead of dealing with that, we decided to implement our own GraphQL client that specifically serves our needs.

Custom GraphQL Client

Here, our custom GraphQL client is described. As part of our Yoli Android App, the client is written in Kotlin. This section focuses on the model classes wrapping the GraphQL requests and responses. For doumentation of the communication flow, refer to the Networking section.

Request

Each GraphQL request (queries and mutations) is stored in a separate file using the .graphql ending to differenciate them from other files. The file names are stored in enum classes, separated by queries and mutations.

For example, this can be the content of the bank_search.graphql file:

query SearchQuery($term: String!) {
  values: bankSearch(search: $term) {
     shortName
     bic
  }
}

To send a request to the Yoli API, a GraphQLBody is built that contains the request text from a .graphql file, as well as the variables that are supplied.

data class GraphQlBody(
        val query: String,
        val variables: Map<String, Any?>? = null
)

The query parameter holds the GraphQL request retrieved from a specified file and the variables parameter holds a map of the supplied variables with the name of the variable as the key and the value of the variable as the value. Note, that you have to check manually if the variable names in this map match the names of the variables in the specified query file.

In order to map the results of a GraphQL request to the data model described in the section below, we need to make use of the alias-feature of GraphQL. In the banksearch example above, the banksearch call is aliased to values to describe that the response will contain a collection of objects. For queries that expect a single object as a response, value is used as the alias.

Response

This is an example response to a GraphQL banksearch query.

{
  "data": {
    "bankSearch": [
      {
        "bankName": "N26 Bank Berlin",
        "city": "Berlin"
      }
    ]
  }
}

Each response to a GraphQL request is wrapped in a GraphQLPage object. This matches the outer {} braces of the above example response.

data class GraphQlPage<out T> (
        val data: T?,
        val errors: List<GraphQlError>?
)

Each GraphQLPage can hold data or a list of errors. The data is wrapped in a ValuePage or a ValuesPage, depending on whether the return type is a collection or a single object. This differenciation is needed for the JSON parsing. The data wrapping matches the aliasing described above.

data class ValuePage<out T> (
        val value: T?
)

data class ValuesPage<out T> (
        val values: List<T>?
)

These Value(s)Pages then hold the actual data that is returned by the request. In the above example, the banksearch field inside the data object would be mapped to a ValuesPage with the type Bank, as the response data contains a list of banks.

Errors are wrapped in GraphQLError objects and contain the errors detail information provided by the Yoli API.

data class GraphQlError(
        val id: String,
        val message: ErrorTranslatable,
        val locations: ArrayList<GraphQlLocation>
)

data class GraphQlLocation(
        val line: Int,
        val column: Int
)

data class ErrorTranslatable(
        val id: String,
        val text: Map<String, String>?
)