December 16, 2021

Introducing Apollo Kotlin

Martin Bonnin

Martin Bonnin

We’re thrilled to announce Apollo Kotlin!

For the past while we’ve been heads down converting Apollo Android to be 100% Kotlin based, which means Apollo Android now works with any Kotlin based application, website or even server. Because of this, we’re renaming the project to Apollo Kotlin!

Apollo Kotlin is a type-safe, caching GraphQL client. It takes your GraphQL queries and generates models that you can use in your application without having to deal with parsing JSON or passing around Maps and making clients cast values to the right type manually.

When Apollo Android started in 2016, it supported Java code generation and a callbacks API.

Apollo Android 1.0.0 introduced Kotlin code generation.

Apollo Android 2.0.0 introduced a separate apollo-runtime-kotlin artifact with support for multiplatform queries and subscriptions.

Apollo Kotlin 3.0.0 takes this one step further and unifies both artifacts into a single apollo-runtime artifact, written in Kotlin and supporting multiplatform cache, multiplatform file upload, multiplatform persisted queries, multiplatform query batching, and multiplatform everything else!

Apollo Kotlin 3 is available today on Maven Central. If you’re coming from Apollo Android 2, check out the migration guide and documentation.

What’s new

We’re not just changing the name! Apollo Kotlin introduces multiple new features and improvements:

Coroutines APIs

Apollo Kotlin exposes coroutines APIs by default.

Queries and Mutations are suspend functions returning an ApolloResponse:

val response = apolloClient.query(MyQuery()).execute()

println(response.data)
val response = apolloClient.mutation(MyMutation()).execute()

println(response.data)

Subscriptions are Flow<ApolloResponse>:

apolloClient.subscription(MySubscription()).toFlow().collect { response ->
  println(response.data)
}

Multiplatform support

Apollo Kotlin code is multiplatform by default with expect/actual implementations for platform dependencies. For example, HTTP is handled by the following components:

PlatformHTTP Client
AndroidOkHttp
JavaScriptKtor
AppleNSURLSesssion

For persistence, Apollo Kotlin uses SQLDelight.

The current feature matrix is:

jvmapplejslinux
apollo-api (models)
apollo-runtime (network, query batching, apq, …)🚫
apollo-normalized-cache🚫
apollo-adapters🚫
apollo-normalized-cache-sqlite🚫🚫
apollo-http-cache🚫🚫🚫

If your favorite platform isn’t listed above, feel free to reach out or contribute it. We’re definitely planning on adding other targets.

responseBased codegen

Apollo Kotlin’s codegen has been refactored to support Fragments as Interfaces.

responseBased models map 1:1 with the JSON response, meaning you don’t have to type .fragments anymore. Given the following query:

query GetHero {
  hero {
    name
    ... on Droid {
      primaryFunction
    }
    ...humanFragment
  }
}
fragment humanFragment on Human {
  height
}

The codegen will generate 3 models that you can use like so:

when (val hero = response.data?.hero) {
  is DroidHero -> println("${hero.name} function is ${hero.primaryFunction}")
  is HumanHero -> println("${hero.name} height is ${hero.height}")
  is OtherHero -> println("${hero.name} is of type ${hero.__typename}")
}

While this looks simpler on the surface, responseBased codegen has to unroll all fragments to build models for each response shape. This generates code that grows exponentially as nested fragments are used (see #3144).

For this reason, responseBased codegen is not enabled by default. You can opt-in with the codegenModels Gradle property:

apollo {
    // Generate models that map 1:1 with the Json response
    codegenModels.set("responseBased") // "operationBased" by default
}
  • responseBased generates more complex models that map the JSON response 1:1.
  • operationBased generates simpler models that map the GraphQL operation 1:1. (default)
  • compat is used for backward compatibility with Apollo Android 2.

responseBased models map the JSON response and merge fields, which means they can always use streaming parsers and never have to rewind. Since they merge fields however, they also need to unroll all the fragments, generating models that grow exponentially in size as nested fragments are used.

There are tradeoffs involved in picking the right codegen approach to use. If you make limited use of fragments, responseBased codegen will be faster and expose more type information. If you have a lot of fragments it can create a lot of generated source, leading to increased build times and increased class loading times.

Read more in the documentation.

SQLite batching

Apollo Kotlin batches SQL requests instead of executing them sequentially. This can speed up reading a complex query by a factor of 2x+ (benchmarks). This is especially true for queries that contain lists:

{
  "data": {
    "launches": {
      "launches": [
        {
          "id": "0",
          "site": "CCAFS SLC 40"
        },
        ...
        {
          "id": "99",
          "site": "CCBGS 80"
        }
      ]
    }
  }
}

Reading the above data from the cache would take 103 SQL queries with Apollo Android 2 (1 for the root, 1 for data, 1 for launches, 1 for each launch). Apollo Kotlin 3 uses 4 SQL queries, executing all the launches at the same time.

Test builders

Test Builders allow generating fake data using a type safe DSL and provide mock values for fields so that you don’t have to specify them all.

Test builders are opt-in. To enable test builders, add the following to your Gradle scripts:

apollo {
  generateTestBuilders.set(true)
}

This will generate builders and add them to your test sourceSets. You can use them to generate fake data:

// Import the generated TestBuilder
import com.example.test.SimpleQuery_TestBuilder.Data

@Test
fun test() {
  // Data is an extension function that will build a SimpleQuery.Data model
  val data = SimpleQuery.Data {
    // Specify values for fields that you want to control
    hero = droidHero {
      name = "R2D2"
      friends = listOf(
          friend {
            name = "Luke"
          }
      )
      // leave other fields untouched, and they will be returned with mocked data
      // planet = ...
    }
  }

  // Use the returned data
}

You can control the returned mock data using the TestResolver API:

val myTestResolver = object: DefaultTestResolver() {
  fun resolveInt(path: List<Any>): Int {
    // Always return 42 in fake data for Int fields
    return 42
  }
}

val data = SimpleQuery.Data(myTestResolver) {}
// Yay, now every Int field in `data` is 42!

@typePolicy and @fieldPolicy directives

@typePolicy and @fieldPolicy are two new directives that make it possible to define cache keys in a declarative, compile-time checked way.

You define @typePolicy by adding an extra type extension to a extra.graphqls file next to your schema. If you have a Book type in your schema, and you want the isbn to be the cache key, you can do so like this:

extend type Book @typePolicy(keyFields: "isbn")

Because this happens at compile type, the codegen will automatically query isbn on every Book field queried.

Symmetrically, you can use @fieldPolicy to tell the runtime how to compute a cache key from a field and query variables.

Given this schema:

type Query {
    book(isbn: String!): Book
}

you can tell the runtime to use the isbn argument as a cache key with:

extend type Query @fieldPolicy(forField: "book", keyArgs: "isbn")

@typePolicy is used after a network request and will make sure your data is de-duplicated. @fieldPolicy is used before a network request. It is an optional optimization that will save a network roundtrip.

Read more in the documentation.

@nonnull directive

@nonnull turns nullable GraphQL fields into non-null Kotlin properties. This directive can be used when a field being null is generally the result of a larger error that you want to catch during parsing.

query GetHero {
  # data.hero will be non-null
  hero @nonnull {
    name
  }
}

Like @typePolicy and @fieldPolicy, @nonnull can also be specified on schema types if you want the same rule to apply to all the fields of a certain type. To do so, add a type extension in your extra.graphqls file:

extend type Query @nonnull(fields: "hero")

Read more in the documentation.

Java compatibility

While Apollo Kotlin is written in Kotlin and has Kotlin-first APIs, it can still generate Java models and work with Java codebases.

apollo-api

Like Apollo Android 2, the apollo-api artifact depends on kotlin-stdlib. You can use your own HTTP client with no other dependencies (documentation):

// build a body
Buffer buffer = new Buffer();
JsonWriter jsonWriter = new BufferedSinkJsonWriter(buffer);
Operations.composeJsonRequest(query, jsonWriter);
String body = buffer.readUtf8();

// send it to your backend
HttpResponse httpResponse = sendHttpRequest(
    "POST",
    "https://com.example/graphql",
    "application/json",
    body
);

// and parse the response
JsonReader jsonReader = BufferedSourceJsonReader(httpResponse.body.source());
ApolloResponse<MyQuery.Data> apolloResponse = Operations.parseJsonResponse(query, jsonReader);

apollo-runtime

Unlike Apollo Android 2, the apollo-runtime artifact now depends on kotlinx.coroutines. To call the Apollo APIs from Java, you can use the RxJava bindings:

ApolloCall<MyQuery.Data> queryCall = client.query(new MyQuery());
Single<ApolloResponse<MyQuery.Data>> queryResponse = Rx2Apollo.single(queryCall);
queryResponse.subscribe( /* ... */ );

We realize that working with coroutines from Java is not always ideal, especially if you’re using a lot of the more advanced APIs like interceptors, store, etc… If you’re in this case, we recommend you stay with Apollo Android 2 and upvote #3694 for more idiomatic Java APIs

Get it!

Apollo Kotlin 3 is available today on Maven Central. If you’re coming from Apollo Android 2, check out the migration guide and documentation.

To get started, add the following to your build.gradle[.kts]:

plugins {
  id("com.apollographql.apollo3").version("3.0.0")
}

dependencies {
  implementation("com.apollographql.apollo3:apollo-runtime:3.0.0")
}

apollo {
  packageName.set("com.example")
}

Download your schema:

./gradlew downloadApolloSchema \
  --endpoint="https://your.domain/graphql/endpoint" \
  --schema="app/src/main/graphql/com/example/schema.graphqls"

Write a query in a ${module}/src/main/graphql/GetRepository.graphql file:

query HeroQuery($id: String!) {
  hero(id: $id) {
    id
    name
    appearsIn
  }
}

Build your project; this will generate a HeroQuery class that you can use with an instance of ApolloClient:

  // Create a client
  val apolloClient = ApolloClient.Builder()
      .serverUrl("https://example.com/graphql")
      .build()

  // Execute your query. This will suspend until the response is received.
  val response = apolloClient.query(HeroQuery(id = "1")).execute()

  println("Hero.name=${response.data?.hero?.name}")

Join the community

Have a question? Want to discuss GraphQL, Kotlin, or the ROADMAP? Feel free to reach out:

Written by

Martin Bonnin

Martin Bonnin

Read more by Martin Bonnin