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.apollo → com.apollographql.apollo3, see section below). Remove apollo-coroutines-support and apollo-android-support if applicable.

  2. Gradle configuration:

Kotlin
1apollo {
2  // Remove this
3  generateKotlinModels.set(true)
4
5  // Add this
6  useVersion2Compat()
7}
  1. Apollo Client configuration:

Kotlin
1val client = ApolloClient.builder()
2  .serverUrl(...)
3
4  // Replace:
5  .addCustomTypeAdapter(CustomType.YOURTYPE, ...)
6
7  // With:
8  .addCustomScalarAdapter(YourType.type, ...)
9
10  .build()
11

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:

Kotlin
1// Replace:
2implementation("com.apollographql.apollo:apollo-runtime:$version")
3implementation("com.apollographql.apollo:apollo-api:$version")
4
5// With:
6implementation("com.apollographql.apollo3:apollo-runtime:$version")
7implementation("com.apollographql.apollo3:apollo-api:$version")

Gradle plugin id

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

Kotlin
1// Replace:
2plugins {
3  id("com.apollographql.apollo").version("$version")
4}
5
6// With:
7plugins {
8  id("com.apollographql.apollo3").version("$version")
9}

Package name

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

Kotlin
1// Replace:
2import com.apollographql.apollo.ApolloClient
3
4// With:
5import 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:

Kotlin
1apollo {
2  // remove this
3  generateKotlinModels.set(true)
4}

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:

Kotlin
1// Remove:
2implementation("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.

Kotlin
1// Remove:
2implementation("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.

Groovy
Kotlin
1apollo {
2  // Replace
3  customTypeMapping = [
4    "GeoPoint" : "com.example.GeoPoint",
5    "Date" : "com.example.MyDate"
6  ]
7  // With
8  mapScalar("GeoPoint", "com.example.GeoPoint")
9  mapScalar("Date", "com.example.MyDate")
10}

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:

Kotlin
1apollo {
2  // Replace
3  graphqlSourceDirectorySet.srcDirs += "shared/graphql"
4
5  // With
6  srcDir("shared/graphql")
7
8  // Replace
9  graphqlSourceDirectorySet.include("**/*.graphql")
10  graphqlSourceDirectorySet.exclude("**/schema.graphql")
11
12  // With
13  includes.add("**/*.graphql")
14  excludes.add("**/schema.graphql")
15}

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:

Kotlin
1apollo {
2  // Replace
3  schemaFile.set(file("src/main/graphql-api/schema.graphqls"))
4
5  // With
6  // Keep schemaFile
7  schemaFile.set(file("src/main/graphql-api/schema.graphqls"))
8  // explicitly set srcDir
9  srcDir(file("src/main/graphql-api/"))
10}

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:

Kotlin
1apollo {
2  packageName.set("com.example")
3}

The generated classes will be:

Text
1- com.example.SomeQuery
2- com.example.fragment.SomeFragment
3- com.example.type.SomeInputObject
4- com.example.type.SomeEnum
5- 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:

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

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

Kotlin
1apollo {
2  packageNameGenerator.set(customPackageNameGenerator)
3}

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

Kotlin
1// Replace
2val apolloClient = ApolloClient.builder()
3    .serverUrl(serverUrl)
4    // ...other Builder methods
5    .build()
6
7// With
8val apolloClient = ApolloClient.Builder()
9    .serverUrl(serverUrl)
10        // ...other Builder methods
11        .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.

Kotlin
1// Replace
2apolloClient.query(query).await()
3// With
4apolloClient.query(query).execute()
5
6// Replace
7apolloClient.mutate(query).await()
8// With
9apolloClient.mutation(query).execute()
10
11// Replace
12apolloClient.subscribe(query).toFlow()
13// With
14apolloClient.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:

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

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:

Kotlin
1// Replace
2val dateAdapter = object : CustomTypeAdapter<Date> {
3  override fun decode(value: CustomTypeValue<*>): Date {
4    return DATE_FORMAT.parse(value.value.toString())
5  }
6
7  override fun encode(value: Date): CustomTypeValue<*> {
8    return GraphQLString(DATE_FORMAT.format(value))
9  }
10}
11
12// With
13val dateAdapter = object : Adapter<Date> {
14  override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): Date {
15    return DATE_FORMAT.parse(reader.nextString())
16  }
17
18  override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: Date) {
19    writer.value(DATE_FORMAT.format(value))
20  }
21}

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:

Kotlin
1// Use AnyAdapter to convert between JsonReader/JsonWriter and a Kotlin Any value
2val geoPointAdapter = object : Adapter<GeoPoint> {
3  override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): GeoPoint {
4    val map = AnyAdapter.fromJson(reader) as Map<String, Double>
5    return GeoPoint(map["latitude"] as Double, map["longitude"] as Double)
6  }
7
8  override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: GeoPoint) {
9    val map = mapOf(
10        "latitude" to value.latitude,
11        "longitude" to value.longitude
12    )
13    AnyAdapter.toJson(writer, map)
14  }
15}

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:

Kotlin
1// Replace
2val apolloClient = apolloClientBuilder.addCustomTypeAdapter(CustomType.DATE, dateAdapter).build()
3
4// With
5val apolloClient = apolloClientBuilder.addCustomScalarAdapter(Date.type, dateAdapter).build()

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

Codegen

Apollo Kotlin 3.x provides 3 codegen options:

  • operationBased: the Kotlin models map the sent GraphQL operation.

  • responseBased: the Kotlin models map the received Json response.

  • compat: for compatibility with Apollo Kotlin 2.x

The compat codegen duplicates some fields and introduce an extra .fragments field. While it's an easy way to migrate from 2.x, we recommend migrating to operationBased once your project is running in compat mode.

operationBased codegen is simpler, de-duplicates some fields and removes the intermediate .fragments field.

Given the following query:

GraphQL
1fragment humanDetails on Human {
2  height
3}
4
5query GetHero {
6  hero {
7    name
8    ... on Droid {
9      primaryFunction
10    }
11    ...humanDetails
12  }
13}

You can migrate to operationBased like so:

Kotlin
1// parent fields are not collected in inline fragments
2// Replace
3hero.asDroid?.name
4// With
5hero.name
6
7// because parent fields are not collected, inline fragments
8// are now named "OnFoo" instead of "AsFoo"
9// Replace
10hero.asDroid?.primaryFunction
11// With
12hero.onDroid?.primaryFunction
13
14// there is no .fragment synthetic field for named fragments
15// Replace
16hero.fragments?.humanDetails?.height
17// With
18hero.humanDetails?.height

We recommend operationBased codegen for most projects.

To go further, and if you're you're comfortable with operationBased codegen, you can read more about the different codegens (including the tradeoffs of using responseBased) in the design documents.

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

Kotlin
1dependencies {
2  // Replace
3  implementation("com.apollographql.apollo:apollo-normalized-cache:$version") // for memory cache
4  implementation("com.apollographql.apollo:apollo-normalized-cache-sqlite:$version") // for SQL cache
5
6  // With
7  implementation("com.apollographql.apollo3:apollo-normalized-cache:$version") // for memory cache
8  implementation("com.apollographql.apollo3:apollo-normalized-cache-sqlite:$version") // for SQL cache
9}
Kotlin
1// Replace
2val cacheFactory = LruNormalizedCacheFactory(
3                     EvictionPolicy.builder().maxSizeBytes(10 * 1024 * 1024).build()
4                   )
5
6val apolloClient = ApolloClient.builder()
7  .serverUrl("https://...")
8  .normalizedCache(cacheFactory)
9  .build()
10
11// With
12val cacheFactory = MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024)
13val apolloClient = ApolloClient.Builder()
14  .serverUrl("https://...")
15  .normalizedCache(cacheFactory)
16  .build()

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

Kotlin
1// Replace
2val response = apolloClient.query(query)
3                      .toBuilder()
4                      .responseFetcher(ApolloResponseFetchers.CACHE_FIRST)
5                      .build()
6                      .await()
7
8// With
9val response = apolloClient.query(request)
10                      .fetchPolicy(CacheFirst)
11                      .execute()

Watchers

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

Kotlin
1val response = apolloClient.query(query)
2                      .watcher()
3                      .toFlow()
4
5// With
6val response = apolloClient.query(query)
7                      .refetchPolicy(CacheFirst)
8                      .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.

Kotlin
1// Replace
2val resolver: CacheKeyResolver = object : CacheKeyResolver() {
3  override fun fromFieldRecordSet(field: ResponseField, recordSet: Map<String, Any>): CacheKey {
4    return CacheKey.from(recordSet["id"] as String)
5  }
6  override fun fromFieldArguments(field: ResponseField, variables: Operation.Variables): CacheKey {
7    return CacheKey.from(field.resolveArgument("id", variables) as String)
8  }
9}
10val apolloClient = ApolloClient.builder()
11    .serverUrl("https://...")
12    .normalizedCache(cacheFactory, resolver)
13    .build()
14
15
16// With
17val cacheKeyGenerator = object : CacheKeyGenerator {
18  override fun cacheKeyForObject(obj: Map<String, Any?>, context: CacheKeyGeneratorContext): CacheKey? {
19    return obj["id"]?.toString()?.let { CacheKey(it) } ?: TypePolicyCacheKeyGenerator.cacheKeyForObject(obj, context)
20  }
21}
22
23val cacheKeyResolver = object : CacheKeyResolver() {
24  override fun cacheKeyForField(field: CompiledField, variables: Executable.Variables): CacheKey? {
25    return (field.resolveArgument("id", variables) as String?)?.let { CacheKey(it) }
26  }
27}
28
29val apolloClient = ApolloClient("https://").normalizedCache(
30    normalizedCacheFactory = cacheFactory,
31    cacheKeyGenerator = cacheKeyGenerator,
32    cacheResolver = cacheKeyResolver
33)

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:

Kotlin
1try {
2  apolloClient.query(query)
3    .fetchPolicy(CacheOnly)
4} catch (e: ApolloException) {
5  // handle error
6}

HTTP cache

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

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

Similarly, the HTTP cache is configurable through extension functions:

Kotlin
1// Replace
2val cacheStore = DiskLruHttpCacheStore()
3val apolloClient = ApolloClient.builder()
4    .serverUrl("/")
5    .httpCache(ApolloHttpCache(cacheStore))
6    .build()
7
8// With
9val apolloClient = ApolloClient.Builder()
10    .serverUrl("https://...")
11    .httpCache(File(cacheDir, "apolloCache"), 1024 * 1024)
12    .build()

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

Kotlin
1// Replace
2val response = apolloClient.query(query)
3                      .toBuilder()
4                      .httpCachePolicy(HttpCachePolicy.CACHE_FIRST)
5                      .build()
6                      .await()
7
8// With
9val response = apolloClient.query(request)
10                      .httpFetchPolicy(CacheFirst)
11                      .execute()

To make it consistent with the normalized cache, the default httpFetchPolicy is now HttpFetchPolicy.CacheFirst. To keep the same behaviour as 2.0, use HttpFetchPolicy.NetworkOnly.

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.

Kotlin
1// Replace
2Input.fromNullable(value)
3// With
4Optional.Present(value)
5
6// Replace
7Input.absent()
8// With
9Optional.Absent
10
11// Replace
12Input.optional(value)
13// With
14Optional.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:

Kotlin
1apollo {
2  // ...
3  generateOptionalOperationVariables.set(false)
4}

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

GraphQL
1query GetHero($id: String @optional) {
2  hero(id: $id)
3}

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:

GraphQL
1# This now generates valid Kotlin code
2enum Direction {
3  left @deprecated("use LEFT instead")
4  LEFT,
5  RIGHT
6}

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:

Kotlin
1// Replace
2val idlingResource = ApolloIdlingResource.create("ApolloIdlingResource", apolloClient)
3IdlingRegistry.getInstance().register(idlingResource)
4
5// With
6val idlingResource = ApolloIdlingResource("ApolloIdlingResource")
7IdlingRegistry.getInstance().register(idlingResource)
8val apolloClient = ApolloClient.Builder()
9    .serverUrl("https://example.com/graphql")
10    .idlingResource(idlingResource)
11    .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:

Kotlin
1dependencies {
2  implementation("com.apollographql.apollo3:apollo-adapters:x.y.z")
3}
4
5apollo {
6    mapScalar(
7        "Decimal",
8        "com.apollographql.apollo3.adapter.BigDecimal",
9        "com.apollographql.apollo3.adapter.BigDecimalAdapter"
10    )
11}

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:

Kotlin
1    val customTypeAdapter = object: CustomTypeAdapter<Address> {
2      override fun decode(value: CustomTypeValue<*>): Address {
3        check (value is CustomTypeValue.GraphQLJsonObject)
4
5        val street = value.value["street"] as String
6
7        // BigDecimal is not exposed anymore
8        // Replace
9        // val number = value.value["number"] as BigDecimal
10
11        // With
12        val number = value.value["number"] as Int
13
14        return Address(street, number)
15      }
16    }

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

Kotlin
1    val apolloClient = ApolloClient.Builder()
2        .serverUrl(mockServer.url())
3        .logCacheMisses() // Put this before setting up your normalized cache
4        .normalizedCache(MemoryCacheFactory())
5        .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 were previously using subscriptionConnectionParams to authenticate your subscription, you can now use wsProtocol with a connectionPayload passed inside the SubscriptionWsProtocol.Factory. More information can be found in Authenticating your WebSockets.

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:

Text
1# schema is now interpreted relative to the root project directory and
2# not the current working directory anymore. This example assumes there
3# is a 'app' module that applies the apollo plugin
4./gradlew downloadApolloSchema \
5  --endpoint="https://your.domain/graphql/endpoint" \
6  --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:

Kotlin
1// Replace
2response.executionContext[OkHttpExecutionContext].response.code
3
4// With
5response.executionContext[HttpInfo]?.statusCode

Using models without the runtime

The API have changed to allow streaming use cases. To compose a request body:

Kotlin
1// Replace
2val request = query.composeRequestBody()
3
4// With
5val request = buildJsonString {
6  query.composeJsonRequest(this, CustomScalarAdapters.Empty)
7}

To parse a response body:

Kotlin
1// Replace
2val response = query.parse(bufferedSource);
3
4// With
5val response = query.parseJsonResponse(bufferedSource);

refetchQueries()

Apollo Android 2.x exposes a ApolloCall.Builder.refetchQueries(List<Query>). This was used to get new values from the network after a mutation that potentially impacts other queries. This gave little control over error handling, caching or concurrency.

Apollo Kotlin 3.x exposes a coroutine API that make it easy to implement similar functionality without library support. Coroutines make it easy to chain calls, retry in case of error or change the dispatcher. For a simple version, you can do something like this:

Kotlin
1  suspend fun <MutationData: Mutation.Data> mutation(mutation: Mutation<MutationData>, vararg refetchQueries: Query<*>) {
2    apolloClient.mutation(mutation).execute()
3    refetchQueries.forEach {
4      apolloClient.query(it).execute()
5    }
6  }  

If you want to bind the refetches to the scope of your ApolloClient, you can do:

Kotlin
1  suspend fun <MutationData: Mutation.Data> mutation(mutation: Mutation<MutationData>, vararg refetchQueries: Query<*>) {
2    apolloClient.mutation(mutation).execute()
3    apolloClient.executionContext[ConcurrencyInfo]!!.coroutineScope.launch {
4      refetchQueries.forEach {
5        apolloClient.query(it).execute()
6      }
7    }
8  }

It is not planned to add refetchQueries() in the library.

Feedback

Edit on GitHub

Forums