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
Update your dependencies and imports (
com.apollographql.apollo
→com.apollographql.apollo3
, see section below). Removeapollo-coroutines-support
andapollo-android-support
if applicable.Gradle configuration:
1apollo {
2 // Remove this
3 generateKotlinModels.set(true)
4
5 // Add this
6 useVersion2Compat()
7}
Apollo Client configuration:
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
withcom.apollographql.apollo3
.
Group id
The maven group id used to identify Apollo Kotlin 3.0 artifacts is com.apollographql.apollo3
:
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
:
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:
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
If you are using the Kotlin Gradle Plugin, Apollo Kotlin 3.0 generates Kotlin models by default. You can safely remove this behavior:
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:
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.
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.
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}
1apollo {
2 // Replace
3 customTypeMapping.set(mapOf(
4 "GeoPoint" to "com.example.GeoPoint",
5 "Date" to "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
:
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:
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:
1apollo {
2 packageName.set("com.example")
3}
The generated classes will be:
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:
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
:
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
).
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.
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
forkotlinx.datetime.Instant
ISO8601 datesJavaInstantAdapter
forjava.time.Instant
ISO8601 datesKotlinxLocalDateAdapter
forkotlinx.datetime.LocalDate
ISO8601 datesJavaLocalDateAdapter
forjava.time.LocalDate
ISO8601 datesKotlinxLocalDateTimeAdapter
forkotlinx.datetime.LocalDateTime
ISO8601 datesJavaLocalDateTimeAdapter
forjava.time.LocalDateTime
ISO8601 datesJavaOffsetDateTimeAdapter
forjava.time.OffsetDateTime
ISO8601 datesDateAdapter
forjava.util.Date
ISO8601 datesBigDecimalAdapter
for multiplatformcom.apollographql.apollo3.adapter.BigDecimal
class holding big decimal values
To include them, add this dependency to your gradle file:
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:
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:
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:
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:
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:
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
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}
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:
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:
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 toCacheKeyGenerator.cacheKeyForObject
.fromFieldArguments
is renamed toCacheKeyResolver.cacheKeyForField
.The
CacheKey
return value is now nullable, andCacheKey.NONE
is replaced withnull
.
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:
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:
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:
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:
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.
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:
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:
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:
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:
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:
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
:
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()
:
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 usinggraphql-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:
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:
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:
1// Replace
2val request = query.composeRequestBody()
3
4// With
5val request = buildJsonString {
6 query.composeJsonRequest(this, CustomScalarAdapters.Empty)
7}
To parse a response body:
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:
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:
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.