/apollo-android

:pager: A strongly-typed, caching GraphQL client for Android, written in Java

Primary LanguageJavaMIT LicenseMIT

Apollo GraphQL Client for Android

GitHub license Get on Slack Build status GitHub release

Apollo-Android is a GraphQL compliant client that generates Java models from standard GraphQL queries. These models give you a typesafe API to work with GraphQL servers. Apollo will help you keep your GraphQL query statements together, organized, and easy to access from Java. Change a query and recompile your project - Apollo code gen will rebuild your data model. Code generation also allows Apollo to read and unmarshal responses from the network without the need of any reflection (see example generated code below). Future versions of Apollo-Android will also work with AutoValue and other value object generators.

Apollo-Android is designed primarily with Android in mind but you can use it in any java/kotlin app. The android-only parts are in apollo-android-support and are only needed to use SQLite as a cache or the android main thread for callbacks.

Table of Contents

Adding Apollo to your Project

The latest Gradle plugin version is Download

To use this plugin, add the dependency to your project's build.gradle file:

buildscript {
  repositories {
    jcenter()
  }
  dependencies {
    classpath 'com.apollographql.apollo:apollo-gradle-plugin:x.y.z'
  }
}

Then add the following to your app's build.gradle dependencies:

repositories {
    jcenter()
}

dependencies {
  implementation 'com.apollographql.apollo:apollo-runtime:x.y.z'
}

Latest development changes are available in Sonatype's snapshots repository:

buildscript {
  repositories {
    jcenter()
    maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
  }
  dependencies {
    classpath 'com.apollographql.apollo:apollo-gradle-plugin:1.0.3-SNAPSHOT'
  }
}
repositories {
    jcenter()
    maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}

dependencies {
  implementation 'com.apollographql.apollo:apollo-runtime:1.0.3-SNAPSHOT'
}

The plugin can then be applied as follows within your app module's build.gradle :

apply plugin: 'com.apollographql.android'

The Android Plugin must be applied before the Apollo plugin. For Kotlin users, the Apollo plugin must be applied before the Kotlin plugins.

NOTE: Apollo Gradle plugin requires Gradle 4.3 or higher.

Generate Code using Apollo

The code generated by the plugin uses jetbrains annotations, so you will need to include this as a compile time dependency in your project's build.gradle:

dependencies {
  compileOnly 'org.jetbrains:annotations:13.0'
  testCompileOnly 'org.jetbrains:annotations:13.0'
}

Follow these steps:

  1. Create the directory graphql inside the maindirectory and create new directory structure like com/apollographql/apollo/sample/so that the Apollo plugin can generate java classes with valid package.
  2. Put your GraphQL queries in a .graphql file. For the sample project in this repo you can find the graphql file at apollo-sample/src/main/graphql/com/apollographql/apollo/sample/GithuntFeedQuery.graphql.
query FeedQuery($type: FeedType!, $limit: Int!) {
  feed(type: $type, limit: $limit) {
    comments {
      ...FeedCommentFragment
    }
    repository {
      ...RepositoryFragment
    }
    postedBy {
      login
    }
  }
}

fragment RepositoryFragment on Repository {
  name
  full_name
  owner {
    login
  }
}

fragment FeedCommentFragment on Comment {
  id
  postedBy {
    login
  }
  content
}

Note: There is nothing Android specific about this query, it can be shared with other GraphQL clients as well

  1. You will also need to add a schema to the project. In the sample project you can find the schema apollo-sample/src/main/graphql/com/apollographql/apollo/sample/schema.json.

You can find instructions to download your schema using the apollo CLI HERE

  1. Compile your project to have Apollo generate the appropriate Java classes with nested classes for reading from the network response. In the sample project, a FeedQuery Java class is created here apollo-sample/build/generated/source/apollo/com/apollographql/apollo/sample.

Note: This is a file that Apollo generates and therefore should not be mutated.

Optional:

  1. Download the JS Graphql Intellij Plugin to update the schema, queries, and mutations on your machine with auto-completion, error highlighting, and go-to-definition functionality. You can create a .graphqlconfig file in order to use GraphQL scratch files to work with your schema outside product code, e.g. by writing temporary queries to test resolvers.

Consuming Code

You can use the generated classes to make requests to your GraphQL API. Apollo includes an ApolloClient that allows you to edit networking options like pick the base url for your GraphQL Endpoint.

In our sample project, we have the base url pointing to https://api.githunt.com/graphql/

There is also a #query && #mutation instance method on ApolloClient that can take as input any Query or Mutation that you have generated using Apollo.

apolloClient.query(
  FeedQuery.builder()
    .limit(10)
    .type(FeedType.HOT)
    .build()
).enqueue(new ApolloCall.Callback<FeedQuery.Data>() {

  @Override public void onResponse(@NotNull Response<FeedQuery.Data> dataResponse) {

    final StringBuffer buffer = new StringBuffer();
    for (FeedQuery.Data.Feed feed : dataResponse.data().feed()) {
      buffer.append("name:" + feed.repository().fragments().repositoryFragment().name());
      buffer.append(" owner: " + feed.repository().fragments().repositoryFragment().owner().login());
      buffer.append(" postedBy: " + feed.postedBy().login());
      buffer.append("\n~~~~~~~~~~~");
      buffer.append("\n\n");
    }

    // onResponse returns on a background thread. If you want to make UI updates make sure they are done on the Main Thread.
    MainActivity.this.runOnUiThread(new Runnable() {
      @Override public void run() {
        TextView txtResponse = (TextView) findViewById(R.id.txtResponse);
        txtResponse.setText(buffer.toString());
      }
    });
      
  }

  @Override public void onFailure(@NotNull Throwable t) {
    Log.e(TAG, t.getMessage(), t);
  }
});       

Custom Scalar Types

Apollo supports Custom Scalar Types like Date.

You first need to define the mapping in your build.gradle file. This will tell the code generator/gradle plugin what type to use when generating the classes.

apollo {
  customTypeMapping = [
    "Date" : "java.util.Date"
  ]
}

Next register your custom adapter & add it to your Apollo Client Builder:

 dateCustomTypeAdapter = new CustomTypeAdapter<Date>() {
      @Override public Date decode(CustomTypeValue value) {
        try {
          return DATE_FORMAT.parse(value.value.toString());
        } catch (ParseException e) {
          throw new RuntimeException(e);
        }
      }

      @Override public CustomTypeValue encode(Date value) {
        return new CustomTypeValue.GraphQLString(DATE_FORMAT.format(value));
      }
    };

ApolloClient.builder()
  .serverUrl(serverUrl)
  .okHttpClient(okHttpClient)
  .addCustomTypeAdapter(CustomType.DATE, dateCustomTypeAdapter)
  .build();

If you have compiler warnings as errors (options.compilerArgs << "-Xlint" << "-Werror") turned on, your custom type will not compile. You can add a switch suppressRawTypesWarning to the apollo plugin configuration which will annotate your generated class with the proper suppression (@SuppressWarnings("rawtypes"):

apollo {
    customTypeMapping = [
      "URL" : "java.lang.String"
    ]
    suppressRawTypesWarning = "true"
}

Support File Upload

Apollo supports file uploading over graphql-multipart-request-spec.

You need to define this mapping in your build.gradle file.

apollo {
  customTypeMapping = [
    "Upload" : "com.apollographql.apollo.api.FileUpload"
  ]
}

In this example, the GraphQL schema uses custom scalar type named Upload for file upload. Change it to match your GraphQL schema.

Create graphql mutation.

mutation SingleUpload($file: Upload!) {
  singleUpload(file: $file) {
    id
    path
    filename
    mimetype
  }
}

Call your mutation with mimetype and a valid File.

  mutationSingle = SingleUploadMutation.builder()
        .file(new FileUpload("image/jpg", new File("/my/image.jpg")))
        .build();

Support For Cached Responses

Apollo GraphQL client allows you to cache responses, making it suitable for use even while offline. The client can be configured with 3 levels of caching:

  • HTTP Response Cache: For caching raw http responses.
  • Normalized Disk Cache: Per node caching of responses in SQL. Persists normalized responses on disk so that they can used after process death.
  • Normalized InMemory Cache: Optimized Guava memory cache for in memory caching as long as the App/Process is still alive.

Usage

To enable HTTP Cache support, add the dependency to your project's build.gradle file. The latest version is Download

dependencies {
  implementation 'com.apollographql.apollo:apollo-http-cache:x.y.z'
}

Raw HTTP Response Cache:

//Directory where cached responses will be stored
File file = new File(context.getApplicationContext().getFilesDir(), "apolloCache");

//Size in bytes of the cache
long size = 1024*1024;

//Create the http response cache store
DiskLruHttpCacheStore cacheStore = new DiskLruHttpCacheStore(file, size); 

//Build the Apollo Client
ApolloClient apolloClient = ApolloClient.builder()
  .serverUrl("/")
  .httpCache(new ApolloHttpCache(cacheStore))
  .okHttpClient(okHttpClient)
  .build();

apolloClient
  .query(
    FeedQuery.builder()
      .limit(10)
      .type(FeedType.HOT)
      .build()
  )
  .httpCachePolicy(HttpCachePolicy.CACHE_FIRST)
  .enqueue(new ApolloCall.Callback<FeedQuery.Data>() {

    @Override public void onResponse(@NotNull Response<FeedQuery.Data> dataResponse) {
      ...
    }

    @Override public void onFailure(@NotNull Throwable t) {
      ...
    }
  }); 

IMPORTANT: Caching is provided only for query operations. It isn't available for mutation operations.

There are four available cache policies HttpCachePolicy:

  • CACHE_ONLY - Fetch a response from the cache only, ignoring the network. If the cached response doesn't exist or is expired, then return an error.
  • NETWORK_ONLY - Fetch a response from the network only, ignoring any cached responses.
  • CACHE_FIRST - Fetch a response from the cache first. If the response doesn't exist or is expired, then fetch a response from the network.
  • NETWORK_FIRST - Fetch a response from the network first. If the network fails and the cached response isn't expired, then return cached data instead.

For CACHE_ONLY, CACHE_FIRST and NETWORK_FIRST policies you can define the timeout after what cached response is treated as expired and will be evicted from the http cache, expireAfter(expireTimeout, timeUnit).`

Normalized Disk Cache:

//Create the ApolloSqlHelper. Please note that if null is passed in as the name, you will get an in-memory SqlLite database that 
// will not persist across restarts of the app.
ApolloSqlHelper apolloSqlHelper = ApolloSqlHelper.create(context, "db_name");

//Create NormalizedCacheFactory
NormalizedCacheFactory cacheFactory = new SqlNormalizedCacheFactory(apolloSqlHelper);

//Create the cache key resolver, this example works well when all types have globally unique ids.
CacheKeyResolver resolver =  new CacheKeyResolver() {
 @NotNull @Override
   public CacheKey fromFieldRecordSet(@NotNull ResponseField field, @NotNull Map<String, Object> recordSet) {
     return formatCacheKey((String) recordSet.get("id"));
   }
 
   @NotNull @Override
   public CacheKey fromFieldArguments(@NotNull ResponseField field, @NotNull Operation.Variables variables) {
     return formatCacheKey((String) field.resolveArgument("id", variables));
   }
 
   private CacheKey formatCacheKey(String id) {
     if (id == null || id.isEmpty()) {
       return CacheKey.NO_KEY;
     } else {
       return CacheKey.from(id);
     }
   }
};

//Build the Apollo Client
ApolloClient apolloClient = ApolloClient.builder()
  .serverUrl("/")
  .normalizedCache(cacheFactory, resolver)
  .okHttpClient(okHttpClient)
  .build();

Normalized In-Memory Cache:

//Create NormalizedCacheFactory
NormalizedCacheFactory cacheFactory = new LruNormalizedCacheFactory(EvictionPolicy.builder().maxSizeBytes(10 * 1024).build());

//Build the Apollo Client
ApolloClient apolloClient = ApolloClient.builder()
  .serverUrl("/")
  .normalizedCache(cacheFactory, resolver)
  .okHttpClient(okHttpClient)
  .build();

Chaining Caches: You can use both a memory cache and sql cache, with a cache chain. Reads will read from the first cache hit in the chain. Writes will propagate down the entire chain.

NormalizedCacheFactory sqlCacheFactory = new SqlNormalizedCacheFactory(apolloSqlHelper)
NormalizedCacheFactory memoryFirstThenSqlCacheFactory = new LruNormalizedCacheFactory(
  EvictionPolicy.builder().maxSizeBytes(10 * 1024).build()
).chain(sqlCacheFactory);

For concrete examples of using response caches, please see the following tests in the apollo-integration module: CacheTest, SqlNormalizedCacheTest, LruNormalizedCacheTest.

RxJava Support

Apollo GraphQL client comes with RxJava1 & RxJava2 support.

Apollo types can be converted to RxJava1 & RxJava2 Observable types using wrapper functions RxApollo.from(...) & Rx2Apollo.from(...) (respectively).

Conversion is done according to the following table:

Apollo type RxJava1 / RxJava2 type
ApolloCall<T> Observable<Response<T>>
ApolloSubscriptionCall<T> Observable<Response<T>>
ApolloQueryWatcher<T> Observable<Response<T>>
ApolloStoreOperation<T> Single<T>
ApolloPrefetch Completable

Including in your project

Add one of the following dependencies:

apollo-rx-support

// RxJava1 support
implementation 'com.apollographql.apollo:apollo-rx-support:x.y.z'

apollo-rx2-support

// RxJava2 support
implementation 'com.apollographql.apollo:apollo-rx2-support:x.y.z'

Usage examples

Converting ApolloCall to an Observable:
//Create a query object
EpisodeHeroName query = EpisodeHeroName.builder().episode(Episode.EMPIRE).build();

//Create an ApolloCall object
ApolloCall<EpisodeHeroName.Data> apolloCall = apolloClient.query(query);

//RxJava1 Observable
Observable<Response<EpisodeHeroName.Data>> observable1 = RxApollo.from(apolloCall);

//RxJava2 Observable
Observable<Response<EpisodeHeroName.Data>> observable2 = Rx2Apollo.from(apolloCall);
Converting ApolloPrefetch to a Completable:
//Create a query object
EpisodeHeroName query = EpisodeHeroName.builder().episode(Episode.EMPIRE).build();

//Create an ApolloPrefetch object
ApolloPrefetch<EpisodeHeroName.Data> apolloPrefetch = apolloClient.prefetch(query);

//RxJava1 Completable
Completable completable1 = RxApollo.from(apolloPrefetch);

//RxJava2 Completable
Completable completable2 = Rx2Apollo.from(apolloPrefetch);

Also, don't forget to dispose of your Observer/Subscriber when you are finished:

Disposable disposable = Rx2Apollo.from(query).subscribe();

//Dispose of your Observer when you are done with your work
disposable.dispose();

As an alternative, multiple Disposables can be collected to dispose of at once via CompositeDisposable:

CompositeDisposable disposable = new CompositeDisposable();
disposable.add(Rx2Apollo.from(call).subscribe());

// Dispose of all collected Disposables at once
disposable.clear();

For a concrete example of using Rx wrappers for apollo types, checkout the sample app in the apollo-sample module.

Gradle Configuration of Apollo Android

Apollo Android comes with logical defaults that will work for the majority of use cases, below you will find additional configuration that will add Optional Support & Semantic Query Naming.

Optional Support

By default Apollo-Android will return null when a graph api returns a null field. Apollo allows you to configure the generated code to instead use a Guava Optional<T> or a shadedApollo Optional<T> rather than simply returning the scalar value or null.

Usage

apollo {
  nullableValueType = "apolloOptional"  //use one or the other
  nullableValueType = "guavaOptional"   //use one or the other
}

Semantic Naming

By default Apollo-Android expects queries to be written as follows: Query someQuery{....} alternatively you can turn on Semantic Naming which will allow you to define queries without the Query suffix: Query some{....}

With Semantic Naming enabled you will still see a SomeQuery.java generated same as the first query above.

Usage

apollo {
  useSemanticNaming = false
}

Java Beans Semantic Naming for Accessors

By default, the generated classes have accessor methods whose names are identical to the name of the Schema field.

query Foo { bar }

results in a class signature like:

class Foo {
    public Bar bar() { ... }
}

Alternatively, turning on Java Beans Semantic Naming will result in those methods being pre-pended with get or is:

class Foo {
    public Bar getBar() { ... }
}

Usage

apollo {
  useJavaBeansSemanticNaming = true
}

Explicit Schema location

By default Apollo-Android tries to lookup GraphQL schema file in /graphql folder, the same folder where all your GraphQL queries are stored. For example, if query files are located at /src/main/graphql/com/example/api then the schema file should be placed in the same location /src/main/graphql/com/example/api. Relative path of schema file to /src/main/graphql root folder defines the package name for generated models, in our example the package name of generated models will be com.example.api.

Alternatively, you can explicitly provide GraphQL schema file location and package name for generated models:

Usage

apollo {
  sourceSet {
    schemaFile = "/path_to_schema_file/my-schema.json"
  }
  outputPackageName = "com.my-example.graphql.api"
}

Exclude GraphQL files

Apollo Gradle plugin supports GraphQL operations defined in *.graphql|*.gql files. You can provide additional configuration to exclude certain GraphQL files by providing file filters.

Usage

apollo {
  sourceSet {
    exclude = "**/*.gql"
  }
  outputPackageName = "com.my-example.graphql.api"
}

If there is more than one filter:

apollo {
  sourceSet {
    exclude = ["**/Query1.graphql", "**/Query2.graphql"]
  }
  outputPackageName = "com.my-example.graphql.api"
}

Use system pre-installed apollo-codegen

By default Apollo will enable gradle plugin that installs Node-JS and downloads apollo-codegen module into your project's build directory. If you already have Node-JS and apollo-codegen module installed on your computer, you can enable Apollo to use it and skip these steps. Apollo will fallback to default behaviour if verification of pre-installed version of apollo-codegen fails.

Usage

To enable usage of pre-installed apollo-codegen module, set gradle system property apollographql.useGlobalApolloCodegen (for example in gradle.properties file):

systemProp.apollographql.useGlobalApolloCodegen=true

Note that this requires exactly version 0.19.1 (not older, not newer). You can install this conventionally via npm install -g apollo-codegen@0.19.1.

Visitor generation for polymorphic datatypes

Apollo Gradle plugin also supports generating visitors for compile-time safe handling of polymorphic datatypes. By default the feature is turned off since it requires source/target compatibility with Java 1.8. To opt into visitor generation:

apollo {
  generateVisitorForPolymorphicDatatypes = true
}

Kotlin model generation (experimental)

By default Apollo Gradle plugin generates Java models but you can configure it to generate Kotlin models instead:

apollo {
  generateKotlinModels = true
}

It is still an experimental and not finalized yet. The structure of generated models is subject to change.

License

The MIT License (MIT)

Copyright (c) 2017 Meteor Development Group, Inc.