Docs
Try Apollo Studio

Migrating to Apollo Kotlin 3.0

From 2.x


Apollo Kotlin 3.0 rewrites most of Apollo Android's internals in Kotlin. Among other improvements, it features:

  • Kotlin-first, coroutine-based APIs
  • A unified runtime for both JVM and multiplatform
  • Declarative cache, @nonnull client directives, performance improvements and more...

Although most of the library's concepts are the same, many APIs have changed to work better in Kotlin.

This page describes the most important changes, along with how to migrate an existing project from Apollo Android 2.x to Apollo Kotlin 3.x.

Feel free to ask questions by either opening an issue on our GitHub repo, joining the community or stopping by our channel in the KotlinLang Slack(get your invite here).

The quick route 🚀

Apollo Kotlin 3 provides a few helpers and compatibility modes to ease the migration from 2.x. To quickly reach a working state, follow the steps below. Once you have a working app, we strongly recommend to migrate to idiomatic Apollo Kotlin 3 as described in the All details section below. The compatibility helpers will be removed in a future version of Apollo Kotlin

  1. Update your dependencies and imports (com.apollographql.apollocom.apollographql.apollo3, see section below). Remove apollo-coroutines-support and apollo-android-support if applicable.
  2. Gradle configuration:
apollo {
// Remove this
generateKotlinModels.set(true)
// Add this
useVersion2Compat()
}
  1. Apollo Client configuration:
val client = ApolloClient.builder()
.serverUrl(...)
// Replace:
.addCustomTypeAdapter(CustomType.YOURTYPE, ...)
// With:
.addCustomScalarAdapter(YourType.type, ...)
.build()

All details

Package name / group id / plugin id

Apollo Kotlin 3.0 uses a new identifier for its package name, Gradle plugin id, and maven group id: com.apollographql.apollo3.

This change avoids dependency conflicts as encouraged in Java Interoperability Policy for Major Version Updates. It also allows to run version 2 and version 3 side by side if needed.

In most cases, you can update the identifier throughout your project by performing a find-and-replace and replacing com.apollographql.apollo with com.apollographql.apollo3.

Group id

The maven group id used to identify Apollo Kotlin 3.0 artifacts is com.apollographql.apollo3:

// Replace:
implementation("com.apollographql.apollo:apollo-runtime:$version")
implementation("com.apollographql.apollo:apollo-api:$version")
// With:
implementation("com.apollographql.apollo3:apollo-runtime:$version")
implementation("com.apollographql.apollo3:apollo-api:$version")

Gradle plugin id

The Apollo Kotlin 3.0 Gradle plugin id is com.apollographql.apollo3:

// Replace:
plugins {
id("com.apollographql.apollo").version("$version")
}
// With:
plugins {
id("com.apollographql.apollo3").version("$version")
}

Package name

All Apollo Kotlin 3.0 classes are imported from the com.apollographql.apollo3 package:

// Replace:
import com.apollographql.apollo.ApolloClient
// With:
import com.apollographql.apollo3.ApolloClient

Gradle configuration

Task names

The Gradle plugin has significantly changed in Apollo Kotlin 3.x to only generate model once for all Android variants. If you were previously using task names like "generateDebugServiceApolloSources", you can now drop the Android variant name and use "generateServiceApolloSources" instead.

generateKotlinModels

Apollo Kotlin 3.0 generates Kotlin models by default. You can safely remove this behavior:

apollo {
// remove this
generateKotlinModels.set(true)
}

apollo-coroutines-support is removed

Apollo Kotlin 3.x is kotlin first and exposes suspend functions by default. apollo-coroutines-support is not needed anymore:

// Remove:
implementation("com.apollographql.apollo:apollo-coroutines-support:$version")

apollo-android-support is removed

Apollo Android 2.x publishes a small artifact to support running callbacks on a specific Handler and write logs to logcat.

Apollo Kotlin 3.x uses coroutines and exposes more information in its API so that logging hooks shouldn't be required any more. If you were using logs to get information about cache hits/misses, you can now catch CacheMissException to get the same information in a more strongly typed way.

// Remove:
implementation("com.apollographql.apollo:apollo-android-support:$version")

Scalar mapping

In order to make it explicit that custom mappings only apply to scalars and not arbitrary types, customTypeMapping has been replaced by the mapScalar method.

apollo {
// Replace
customTypeMapping = [
"GeoPoint" : "com.example.GeoPoint",
"Date" : "com.example.MyDate"
]
// With
mapScalar("GeoPoint", "com.example.GeoPoint")
mapScalar("Date", "com.example.MyDate")
}

Specifying schema and .graphql files

Apollo Android 2.x has a complex logic to determine what files to use as input. For example, it resolves sourceFolder relative to multiple Android variants or kotlin sourceSets, tries to get the .graphql files from the schema location and the other way around too. This logic works in most cases but makes troubleshooting more complicated, especially in more complex scenarios. Also, this runs the GraphQL compiler multiple times for different source sets even if in the vast majority of cases, the same .graphql files are used.

Apollo Kotlin 3.x simplifies this setup. Each Service is exactly one compilation. For Android projects, GraphQL classes are generated once and then added to all variants.

If you previously used graphqlSourceDirectorySet to explicitly specify the location of GraphQL files, you can now use srcDir:

apollo {
// Replace
graphqlSourceDirectorySet.srcDirs += "shared/graphql"
// With
srcDir("shared/graphql")
// Replace
graphqlSourceDirectorySet.include("**/*.graphql")
graphqlSourceDirectorySet.exclude("**/schema.graphql")
// With
includes.add("**/*.graphql")
excludes.add("**/schema.graphql")
}

If you were relying on the schema location to automatically lookup the .graphql files, you should also now add srcDir() to explicitly set the location of your .graphql files:

apollo {
// Replace
schemaFile.set(file("src/main/graphql-api/schema.graphqls"))
// With
// Keep schemaFile
schemaFile.set(file("src/main/graphql-api/schema.graphqls"))
// explicitly set srcDir
srcDir(file("src/main/graphql-api/"))
}

If you need different GraphQL operations for different variants, you can create multiple services for each Android variant using apollo.createAllAndroidVariantServices.

Package name

Apollo Android 2.x computes its target package name based on a combination of the path of GraphQL operation and schema files, and the packageName and rootPackageName options. While this is very flexible, it's not easy to anticipate the final package name that is going to be used.

Apollo Kotlin 3.x uses a flat package name by default using the packageName option:

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

The generated classes will be:

- com.example.SomeQuery
- com.example.fragment.SomeFragment
- com.example.type.SomeInputObject
- com.example.type.SomeEnum
- com.example.type.Types // types is a slimmed down version of the schema

If you need different package names for different operation folders, you can fallback to the 2.x behaviour with:

apollo {
packageNamesFromFilePaths("$rootPackageName")
# If using version 3.1.0+, you will also need useSchemaPackageNameForFragments
useSchemaPackageNameForFragments.set(true)
}

For even more control, you can also define your own PackageNameGenerator:

apollo {
packageNameGenerator.set(customPackageNameGenerator)
}

Builders

On Apollo Android 2.x you would use the ApolloClient.builder() method to instantiate a new Builder. With 3.x, use the ApolloClient.Builder() constructor instead (notice the capital B).

// Replace
val apolloClient = ApolloClient.builder()
.serverUrl(serverUrl)
// ...other Builder methods
.build()
// With
val apolloClient = ApolloClient.Builder()
.serverUrl(serverUrl)
// ...other Builder methods
.build()

Operation APIs

Apollo Android 2.x has callback APIs that can become verbose and require explicitly handling cancellation.

Apollo Kotlin 3.x exposes more concise coroutines APIs that handle cancellation automatically through the coroutine scope.

Also, mutate has been renamed to mutation and subscribe has been renamed to subscription for consistency.

// Replace
apolloClient.query(query).await()
// With
apolloClient.query(query).execute()
// Replace
apolloClient.mutate(query).await()
// With
apolloClient.mutation(query).execute()
// Replace
apolloClient.subscribe(query).toFlow()
// With
apolloClient.subscription(subscription).toFlow()

Custom Scalar adapters

Apollo Kotlin 3 ships an optional apollo-adapters artifact that includes adapters for common scalar types like:

  • KotlinxInstantAdapter for kotlinx.datetime.Instant ISO8601 dates
  • JavaInstantAdapter for java.time.Instant ISO8601 dates
  • KotlinxLocalDateAdapter for kotlinx.datetime.LocalDate ISO8601 dates
  • JavaLocalDateAdapter for java.time.LocalDate ISO8601 dates
  • KotlinxLocalDateTimeAdapter for kotlinx.datetime.LocalDateTime ISO8601 dates
  • JavaLocalDateTimeAdapter for java.time.LocalDateTime ISO8601 dates
  • JavaOffsetDateTimeAdapter for java.time.OffsetDateTime ISO8601 dates
  • DateAdapter for java.util.Date ISO8601 dates
  • BigDecimalAdapter for multiplatform com.apollographql.apollo3.adapter.BigDecimal class holding big decimal values

To include them, add this dependency to your gradle file:

dependencies {
implementation("com.apollographql.apollo3:apollo-adapters:$version")
}

If the above adapters do not fit your needs or if you need to customize them, you can use the Custom Scalar adapters API.

The Custom Scalar adapters API has changed a lot to support nullable and absent values as well as streaming use cases. Apollo Kotlin 3 makes it possible to read/write custom scalars without having to create an intermediate copy in memory. To do this, it uses the same Adapter API that is used internally to parse the models:

// Replace
val dateAdapter = object : CustomTypeAdapter<Date> {
override fun decode(value: CustomTypeValue<*>): Date {
return DATE_FORMAT.parse(value.value.toString())
}
override fun encode(value: Date): CustomTypeValue<*> {
return GraphQLString(DATE_FORMAT.format(value))
}
}
// With
val dateAdapter = object : Adapter<Date> {
override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): Date {
return DATE_FORMAT.parse(reader.nextString())
}
override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: Date) {
writer.value(DATE_FORMAT.format(value))
}
}

The JsonReader and JsonWriter APIs are similar to the ones you can find in Moshi and are stateful APIs that require you to handle the Json properties in the order they arrive from the Json stream. If you prefer, you can also buffer the Json into an untyped Any? value that represent the json and use AnyAdapter to decode/encode it:

// Use AnyAdapter to convert between JsonReader/JsonWriter and a Kotlin Any value
val geoPointAdapter = object : Adapter<GeoPoint> {
override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): GeoPoint {
val map = AnyAdapter.fromJson(reader) as Map<String, Double>
return GeoPoint(map["latitude"] as Double, map["longitude"] as Double)
}
override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: GeoPoint) {
val map = mapOf(
"latitude" to value.latitude,
"longitude" to value.longitude
)
AnyAdapter.toJson(writer, map)
}
}

After you define your adapters, you need to register them with your ApolloClient instance. To do so, call ApolloClient.Builder.addCustomScalarAdapter once for each adapter:

// Replace
val apolloClient = apolloClientBuilder.addCustomTypeAdapter(CustomType.DATE, dateAdapter).build()
// With
val apolloClient = apolloClientBuilder.addCustomScalarAdapter(Date.type, dateAdapter).build()

This method takes a type-safe generated class from Types, along with its corresponding adapter.

Normalized Cache

The Apollo Android 2.x runtime has a dependency on the normalized cache APIs, and it's possible to call cache methods even if no cache implementation is in the classpath.

The Apollo Kotlin 3.x runtime is more modular and doesn't know anything about normalized cache by default. To add normalized cache support, add the dependencies to your gradle file:

Configuration

dependencies {
// Replace
implementation("com.apollographql.apollo:apollo-normalized-cache:$version") // for memory cache
implementation("com.apollographql.apollo:apollo-normalized-cache-sqlite:$version") // for SQL cache
// With
implementation("com.apollographql.apollo3:apollo-normalized-cache:$version") // for memory cache
implementation("com.apollographql.apollo3:apollo-normalized-cache-sqlite:$version") // for SQL cache
}
// Replace
val cacheFactory = LruNormalizedCacheFactory(
EvictionPolicy.builder().maxSizeBytes(10 * 1024 * 1024).build()
)
val apolloClient = ApolloClient.builder()
.serverUrl("https://...")
.normalizedCache(cacheFactory)
.build()
// With
val cacheFactory = MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024)
val apolloClient = ApolloClient.Builder()
.serverUrl("https://...")
.normalizedCache(cacheFactory)
.build()

Configuring the fetch policy is now made on an ApolloCall instance:

// Replace
val response = apolloClient.query(query)
.toBuilder()
.responseFetcher(ApolloResponseFetchers.CACHE_FIRST)
.build()
.await()
// With
val response = apolloClient.query(request)
.fetchPolicy(CacheFirst)
.execute()

Watchers

Watchers now default to a CacheOnly refetchPolicy instead CACHE_FIRST. To keep behaviour unchanged, set a refetchPolicy on your watchers:

val response = apolloClient.query(query)
.watcher()
.toFlow()
// With
val response = apolloClient.query(query)
.refetchPolicy(CacheFirst)
.watch()

CacheKeyResolver

The CacheKeyResolver API has been split in two different APIs:

  • CacheKeyGenerator.cacheKeyForObject
    • takes Json data as input and returns a unique id for an object.
    • is used after a network request
    • is used during normalization when writing to the cache
  • CacheKeyResolver.cacheKeyForField
    • takes a GraphQL field and operation variables as input and generates an id for this field
    • is used before a network request
    • is used when reading the cache

Previously, both methods were in CacheResolver even if under the hood, the code path were very different. By separating them, it makes it explicit and also makes it possible to only implement one of them.

At a high level,

  • fromFieldRecordSet is renamed to CacheKeyGenerator.cacheKeyForObject.
  • fromFieldArguments is renamed to CacheKeyResolver.cacheKeyForField.
  • The CacheKey return value is now nullable, and CacheKey.NONE is replaced with null.
// Replace
val resolver: CacheKeyResolver = object : CacheKeyResolver() {
override fun fromFieldRecordSet(field: ResponseField, recordSet: Map<String, Any>): CacheKey {
return CacheKey.from(recordSet["id"] as String)
}
override fun fromFieldArguments(field: ResponseField, variables: Operation.Variables): CacheKey {
return CacheKey.from(field.resolveArgument("id", variables) as String)
}
}
val apolloClient = ApolloClient.builder()
.serverUrl("https://...")
.normalizedCache(cacheFactory, resolver)
.build()
// With
val cacheKeyGenerator = object : CacheKeyGenerator {
override fun cacheKeyForObject(obj: Map<String, Any?>, context: CacheKeyGeneratorContext): CacheKey? {
return obj["id"]?.toString()?.let { CacheKey(it) } ?: TypePolicyCacheKeyGenerator.cacheKeyForObject(obj, context)
}
}
val cacheKeyResolver = object : CacheKeyResolver() {
override fun cacheKeyForField(field: CompiledField, variables: Executable.Variables): CacheKey? {
return (field.resolveArgument("id", variables) as String?)?.let { CacheKey(it) }
}
}
val apolloClient = ApolloClient("https://").normalizedCache(
normalizedCacheFactory = cacheFactory,
cacheKeyGenerator = cacheKeyGenerator,
cacheResolver = cacheKeyResolver
)

Cache misses now always throw

In Apollo Android 2, a cache miss with a CacheOnly policy returns an ApolloResponse with response.data = null. This was inconsistent with the CacheFirst policy that treats cache misses as errors.

In Apollo Android 3, calling ApolloCall.execute() is guaranteed to always return one valid (maybe partial) ApolloResponse or throw.

If you have any CacheOnly queries, make sure to catch their result:

try {
apolloClient.query(query)
.fetchPolicy(CacheOnly)
} catch (e: ApolloException) {
// handle error
}

HTTP cache

To add http cache support, add the dependency to your gradle file:

dependencies {
// Add
implementation("com.apollographql.apollo3:apollo-http-cache:$version") // Gives access to `httpCache` and `httpFetchPolicy`
}

Similarly, the HTTP cache is configurable through extension functions:

// Replace
val cacheStore = DiskLruHttpCacheStore()
val apolloClient = ApolloClient.builder()
.serverUrl("/")
.httpCache(ApolloHttpCache(cacheStore))
.build()
// With
val apolloClient = ApolloClient.Builder()
.serverUrl("https://...")
.httpCache(File(cacheDir, "apolloCache"), 1024 * 1024)
.build()

Configuring the HTTP fetch policy is now made on an ApolloCall instance:

// Replace
val response = apolloClient.query(query)
.toBuilder()
.httpCachePolicy(HttpCachePolicy.CACHE_FIRST)
.build()
.await()
// With
val response = apolloClient.query(request)
.httpFetchPolicy(CacheFirst)
.execute()

Optional values

The Optional class

Apollo Kotlin distinguishes between null values and absent values.

Apollo Android 2.x uses Input to represent optional (maybe nullable) values for input types.

Apollo Kotlin 3.x uses Optional instead so that later it can potentially be used in places besides input types (for example, fields could be made optional with an @optional directive).

Optional is a sealed class, so when statements don't need an else branch.

// Replace
Input.fromNullable(value)
// With
Optional.Present(value)
// Replace
Input.absent()
// With
Optional.Absent
// Replace
Input.optional(value)
// With
Optional.presentIfNotNull(value)

Non-optional variables generation

By default, the GraphQL spec treats nullable variables as optional, so it's valid to omit them at runtime. In practice, however, this is rarely used and makes the operation's declaration verbose.

That is why Apollo Kotlin 3.x provides a mechanism to remove the Optional wrapper, which makes the construction of queries with nullable variables more straightforward.

When enabled, this new behavior applies only to variables. Fields of input objects still use Optional, because it's common to omit particular input fields.

To omit optional variables globally in your Gradle config:

apollo {
// ...
generateOptionalOperationVariables.set(false)
}

If you still need to omit a variable in certain places, you can opt in to the Optional wrapper with the @optional directive:

query GetHero($id: String @optional) {
hero(id: $id)
}

More information is available here.

Enums

Apollo Android 2.x always converts enums and enum values names to uppercase.

Apollo Kotlin 3.x uses the schema case for both enums and enum value names. This allows to define multiple enum values with a different case:

# This now generates valid Kotlin code
enum Direction {
left @deprecated("use LEFT instead")
LEFT,
RIGHT
}

See #3035 for more details

IdlingResource

In Apollo Android 2.x, you can pass an ApolloClient to ApolloIdlingResource.create() and the ApolloClient will be modified to register an idle callback.

In Apollo Kotlin 3.x, an ApolloClient is immutable once instantiated so it's not possible to register callbacks/interceptors once it is instantiated. Instead, you can create your ApolloIdlingResource as a first step and then pass it to your ApolloClient.Builder like so:

// Replace
val idlingResource = ApolloIdlingResource.create("ApolloIdlingResource", apolloClient)
IdlingRegistry.getInstance().register(idlingResource)
// With
val idlingResource = ApolloIdlingResource("ApolloIdlingResource")
IdlingRegistry.getInstance().register(idlingResource)
val apolloClient = ApolloClient.Builder()
.serverUrl("https://example.com/graphql")
.idlingResource(idlingResource)
.build()

BigDecimal

By default, Apollo Android 2.x internally maps all numbers to a custom BigDecimal value.

For performance reasons, Apollo Kotlin 3.x uses the primitive type when possible and moves BigDecimal to the apollo-adapters artifact.

If you are using big decimal custom scalars in your schema, you should now explicitly add them in your Gradle configuration:

dependencies {
implementation("com.apollographql.apollo3:apollo-adapters:x.y.z")
}
apollo {
mapScalar(
"Decimal",
"com.apollographql.apollo3.adapter.BigDecimal",
"com.apollographql.apollo3.adapter.BigDecimalAdapter"
)
}

If you are not using big decimal custom scalars, you might still be impacted if you are defining your own adapters. In that case, be prepared to receive Int/Double instead of BigDecimal:

val customTypeAdapter = object: CustomTypeAdapter<Address> {
override fun decode(value: CustomTypeValue<*>): Address {
check (value is CustomTypeValue.GraphQLJsonObject)
val street = value.value["street"] as String
// BigDecimal is not exposed anymore
// Replace
// val number = value.value["number"] as BigDecimal
// With
val number = value.value["number"] as Int
return Address(street, number)
}
}

By default, numbers are mapped to Int or Double if they don't fit in an Int. If the number doesn't fit in a Double either, it will fallback to a JsonNumber containing the String representation of the number.

ApolloLogger

Apollo Android 2.x has a ApolloLogger interface. One of the main use cases was to log cache misses.

Apollo Android 3.x removes this interface and exposes the information in APIs instead. APIs offer a more structured and maintainable way to expose data.

For custom logging, you can now use an ApolloInterceptor.

For cache misses, you can use the convenience ApolloClient.Builder.logCacheMisses():

val apolloClient = ApolloClient.Builder()
.serverUrl(mockServer.url())
.logCacheMisses() // Put this before setting up your normalized cache
.normalizedCache(MemoryCacheFactory())
.build()

Subscriptions

Apollo Android 2.x had a SubscriptionManager class. This class exposed a lot of methods and data about the Websocket state management. While very flexible, it was hard to maintain and evolve.

Apollo Android 3.x instead uses WebsocketNetworkTransport to expose a simplified API:

  • protocolFactory allows using graphql-ws or AppSync (or your custom one) as a lowlevel protocol instead of the default.
  • reopenWhen allows to automatically re-subscribe in case of a network error or another error.

If you were previously using apolloClient.subscriptionManager.reconnect() to force a Websocket reconnect (for example to send new authentication parameters), you can now call apolloClient.subscriptionNetworkTransport.closeConnection(<your exception>): this will behave as if a network error occurred, and you can react to this by reconnecting with reopenWhen.

If you feel some API is missing from SubscriptionManager, please reach out so we can discuss the best way to add them.

Downloading schemas

Because it is not possible to determine the Gradle current working directory reliably, the downloadApolloSchema task now uses paths relative to the root project directory:

# schema is now interpreted relative to the root project directory and
# not the current working directory anymore. This example assumes there
# is a 'app' module that applies the apollo plugin
./gradlew downloadApolloSchema \
--endpoint="https://your.domain/graphql/endpoint" \
--schema="app/src/main/graphql/com/example/schema.graphqls"

If you configured the introspection {} or registry {} blocks in your Gradle scripts, you should now use downloadServiceApolloSchemaFromIntrospection or downloadServiceApolloSchemaFromRegistry

OkHttp

Because 3.x is multiplatform first, the runtime abstracts OkHttp behind the HttpEngine API. If you were relying on OkHttpExecutionContext, you can now use HttpInfo to get access to the HTTP response code and headers:

// Replace
response.executionContext[OkHttpExecutionContext].response.code
// With
response.executionContext[HttpInfo]?.statusCode
Edit on GitHub
Previous
Introduction
Next
0. Introduction