The following is the first few sections of a chapter from The Busy Coder's Guide to Android Development, plus headings for the remaining major sections, to give you an idea about the content of the chapter.


Advanced Apollo-Android

Apollo-Android continues to expand its capabilities, to handle broader and deeper GraphQL scenarios in Android apps.

This chapter outlines some additional Apollo-Android features, found starting in v0.4.0 of the library.

Support for Scalar Types

With GraphQL, the server can define custom scalar types. These are marshaled into string values in the JSON. However, if you wish, you can unmarshal them into custom types in your Android app. For example, you could convert GitHub’s DateTime or GitTimestamp scalars into Java DateTime objects.

If you are processing the JSON directly (e.g., dynamic GraphQL with OkHttp and Gson), you would use features of your JSON parser for performing this sort of type conversion. However, if you are using Apollo-Android, you can teach the library how to perform conversions on a per-scalar-type basis. Then, anywhere in the generated code that the scalar type appears, your desired data type will be used for the POJOs.

Of course, this requires a little bit of work, as Apollo-Android cannot magically determine how to convert a string into arbitrary sorts of data. Nor does Apollo-Android know which scalars you want to support this way.

The GitHub/DateTime sample project shows how this is accomplished. This sample app is a clone of the GitHub/Starred sample app shown elsewhere in the book. However, it adds the createdAt field to the GraphQL query:

query myStars($first: Int, $after: String) {
  viewer {
    login
    starredRepositories(first: $first, after: $after, orderBy: {field: STARRED_AT, direction: DESC}) {
      edges {
        cursor
        node {
          id
          name
          createdAt
        }
      }
      pageInfo {
        hasNextPage
      }
    }
  }
}

We want to show that value in the RecyclerView rows.

Gradle Configuration

First, you need to teach the Apollo-Android code generator about the custom scalars that you want to convert and what you want to convert them into. This is done by an apollo closure in your module’s build.gradle file, separate from (and outside of) the android closure:

apply plugin: 'com.android.application'
apply plugin: 'com.apollographql.android'

dependencies {
  compile 'io.reactivex.rxjava2:rxjava:2.0.2'
  compile 'io.reactivex.rxjava2:rxandroid:2.0.0'
  compile 'com.android.support:recyclerview-v7:26.0.1'
  compile 'com.apollographql.apollo:apollo-rx2-support:0.4.0'
}

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.1"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 26
        applicationId "com.commonsware.graphql.github.datetime"
        buildConfigField "String", "GITHUB_TOKEN", '"'+GITHUB_TOKEN+'"'

        jackOptions {
            enabled true
        }
   }

  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

apollo {
  customTypeMapping['DateTime']="java.util.Date"
  useSemanticNaming = false
}

customTypeMapping is a Map-style data structure, listing the scalar type conversions that you want to apply. The key (DateTime) is the name of the scalar type defined by the server. In our case, we want to convert GitHub’s DateTime scalar type. The value (java.util.Date) is the fully-qualified class name of the Java class to use for that type, instead of String.

Implementing Adapters

While the customTypeMapping configuration tells Apollo-Android that you want to convert the scalar to some specific Java type, it does not tell Apollo-Android how you want to perform that conversion. That is handled by a CustomTypeAdapter implementation, akin to similar ones in other libraries (Room, Gson, etc.).

A CustomTypeAdapter needs to implement two methods: encode() and decode(). encode() converts from your requested type into a String to use for creating the JSON to send to the GraphQL endpoint. decode() takes the String and converts it into your requested data type. The String representations should match what the GraphQL endpoint uses.

This project implements an ISO8601Adapter that converts dates encoded in the ISO 8601 format into Java Date objects, and vice versa:

package com.commonsware.graphql.github;

import com.apollographql.apollo.CustomTypeAdapter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

class ISO8601Adapter implements CustomTypeAdapter<Date> {
  private static final SimpleDateFormat ISO8601=
    new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");

  // based in part on https://stackoverflow.com/a/10621553/115145

  @Override
  public Date decode(String value) {
    try {
      return(ISO8601.parse(value.replace("Z", "+00:00")));
    }
    catch (ParseException e) {
      throw new IllegalArgumentException(value+" is not a valid ISO 8601 date", e);
    }
  }

  @Override
  public String encode(Date value) {
    return(ISO8601.format(value));
  }
}

This does not handle the complete ISO 8601 specification, such as microseconds, due to the fact that Java’s date/time handling is a mess. But, this implementation suffices for use with GitHub’s GraphQL API.

Registering Adapters

Then, as part of setting up the ApolloClient, we need to call addCustomTypeAdapter() on the ApolloClient.Builder:

  private ApolloClient apolloClient=ApolloClient.builder()
    .okHttpClient(ok)
    .addCustomTypeAdapter(CustomType.DATETIME, new ISO8601Adapter())
    .serverUrl("https://api.github.com/graphql")
    .build();

addCustomTypeAdapter() takes two parameters:

Then, you can use createdAt in the more-natural Date type rather than as a simple String:

  private static class RowHolder extends RecyclerView.ViewHolder {
    final private TextView name;
    private final DateFormat dateFormat;

    RowHolder(View itemView, DateFormat dateFormat) {
      super(itemView);

      name=(TextView)itemView.findViewById(android.R.id.text1);
      this.dateFormat=dateFormat;
    }

    void bind(MyStars.Node node) {
      name.setText(String.format("%s (%s)", node.name(),
        dateFormat.format(node.createdAt())));
    }
  }