You're viewing documentation for a version of this software that is in development. Switch to the latest stable version
Launch Apollo Studio

Migrating to Apollo Android 3.0

From 2.x


Apollo Android 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 Android 3.x.

The quick route 🚀

Apollo Android 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 Android 3 as described in the All details section below. The compatibility helpers will be removed in a future version of Apollo Android

  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:
  .addCustomTypeAdapter(YourType.type, ...)

  .build()

All details

Package name / group id / plugin id

Apollo Android 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 replapcing com.apollographql.apollo with com.apollographql.apollo3.

Group id

The maven group id used to identify Apollo Android 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 Android 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 Android 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

generateKotlinModels

Apollo Android 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 Android 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 Android 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")

customScalarMappings

In order to make it explicit that custom mappings only apply to custom scalars and not arbitrary types, customTypeMapping has been renamed to customScalarsMapping

apollo {
  // Replace
  customTypeMapping = [
    "GeoPoint" : "com.example.GeoPoint"
  ]
  // With
  customScalarsMapping = [
    "GeoPoint" : "com.example.GeoPoint"
  ]
}

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 Android 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 Android 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")
}

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 Android 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 Android 3 ships an optional apollo-adapters artifact that includes adapters for common scalar types like:

  • InstantAdapter for kotlinx.datetime.Instant ISO8601 dates
  • LocalDateAdapter for kotlinx.datetime.LocalDate ISO8601 dates
  • DateAdapter for java.util.Date ISO8601 dates
  • LongAdapter for java.lang.Long
  • BigDecimalAdapter for a MPP 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 Android 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.

Caching

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 Android 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:

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()

HTTP cache

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()

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 objectIdGenerator = 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,
    objectIdGenerator = objectIdGenerator,
    cacheResolver = cacheKeyResolver
)

Optional values

The Optional class

Apollo Android distinguishes between null values and absent values.

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

Apollo Android 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 by default

By default, Apollo Android 3.x represents operation variables that don't have a default value as non-optional Kotlin constructor parameters.

Given the following query:

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

Apollo Android generates:

// 2.x
class GetHero(val id: Input<String?> = Input.absent())

// 3.x
class GetHero(val id: String?)

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.

Apollo Android 3.x removes the Optional wrapper so that it's more straightforward to construct a query with nullable variables.

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

If you want to be able to omit a variable, you can opt in to the Optional wrapper with the @optional directive:

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

Enums

Apollo Android 2.x converts enum names to uppercase. This is no longer the case with Apollo Android 3.x, which uses the same case for enum names as their GraphQL definition. This enables you to define different enums with different cases, which is valid in GraphQL.

Edit on GitHub