Migrating to Apollo Kotlin 4
From version 3
Apollo Kotlin 3 was a major rewrite of Apollo in Kotlin multiplatform.
Apollo Kotlin 4 focuses on tooling, stability and making the library more maintainable, so it can evolve smoothly for the many years to come.
While most of the core APIs stayed the same, Apollo Kotlin 4 contains a few binary breaking changes. To account for that, and in order to be more future-proof, we changed the package name to com.apollographql.apollo
. See the Apollo Kotlin evolution policy for more details.
Apollo Kotlin 4 removes some deprecated symbols. We strongly recommend updating to the latest 3.x release and removing deprecated usages before migrating to version 4.
Important changes are:
Read below for more details.
Automatic migration using the Android Studio/IntelliJ plugin
Apollo Kotlin 4 ships with a companion Android Studio/IntelliJ plugin that automates most of the migration.
It automates most of the API changes but cannot deal with behavior changes like error handling.
We recommend using the plugin to automate the repetitive tasks but still go through this document for the details.
Group id / plugin id / package name
Apollo Kotlin 4 uses a new identifier for its maven group id, Gradle plugin id and package name : com.apollographql.apollo
.
This also allows to run versions 4 and 3 side by side if needed.
In most cases, you can update the identifier in your project by performing a find-and-replace and replacing com.apollographql.apollo3
with com.apollographql.apollo
.
Group id
The maven group id used to identify Apollo Kotlin 4 artifacts is com.apollographql.apollo
:
1// Replace:
2implementation("com.apollographql.apollo3:apollo-runtime:3.8.4")
3
4// With:
5implementation("com.apollographql.apollo:apollo-runtime:4.0.0")
Gradle plugin id
The Apollo Kotlin 4.0 Gradle plugin id is com.apollographql.apollo
:
1// Replace:
2plugins {
3 id("com.apollographql.apollo3") version "3.8.4"
4}
5
6// With:
7plugins {
8 id("com.apollographql.apollo") version "4.0.0"
9}
Package name
Apollo Kotlin 4 classes use the com.apollographql.apollo
package:
1// Replace:
2import com.apollographql.apollo3.ApolloClient
3import com.apollographql.apollo3.*
4
5// With:
6import com.apollographql.apollo.ApolloClient
7import com.apollographql.apollo.*
Moved artifacts
Over the years, a lot of support functionality was added alongside the apollo-api
and apollo-runtime
artifacts. While useful, most of this functionality doesn't share the same level of stability and maturity as the core artifacts and bundling them did not make much sense.
Moving forward, only the core artifacts remain in the apollo-kotlin
repository and are released together. Other artifacts are moved to new maven coordinates and GitHub repositories.
This will allow us to iterate faster on new functionality while keeping the core smaller and more maintainable.
The artifacts with new coordinates are:
Old coordinates | New coordinates | New Repository |
---|---|---|
com.apollographql.apollo3:apollo-adapters | com.apollographql.adapters:apollo-adapters-core | apollographql/apollo-kotlin-adapters |
com.apollographql.adapters:apollo-adapters-kotlinx-datetime | apollographql/apollo-kotlin-adapters | |
com.apollographql.apollo3:apollo-compose-support-incubating | com.apollographql.compose:compose-support | apollographql/apollo-kotlin-compose-support |
com.apollographql.apollo3:apollo-compose-paging-support-incubating | com.apollographql.compose:compose-paging-support | apollographql/apollo-kotlin-compose-support |
com.apollographql.apollo3:apollo-cli-incubating | com.apollographql.cli:apollo-cli | apollographql/apollo-kotlin-cli |
com.apollographql.apollo3:apollo-engine-ktor | com.apollographql.ktor:apollo-engine-ktor | apollographql/apollo-kotlin-ktor-support |
com.apollographql.apollo3:apollo-mockserver | com.apollographql.mockserver:apollo-mockserver | apollographql/apollo-kotlin-mockserver |
com.apollographql.apollo3:apollo-normalized-cache-incubating | com.apollographql.cache:normalized-cache-incubating | apollographql/apollo-kotlin-normalized-cache-incubating |
com.apollographql.apollo3:apollo-normalized-cache-api-incubating | com.apollographql.cache:normalized-cache-incubating | apollographql/apollo-kotlin-normalized-cache-incubating |
com.apollographql.apollo3:apollo-normalized-cache-sqlite-incubating | com.apollographql.cache:normalized-cache-sqlite-incubating | apollographql/apollo-kotlin-normalized-cache-incubating |
com.apollographql.apollo3:apollo-runtime-java | com.apollographql.java:client | apollographql/apollo-kotlin-java-support |
com.apollographql.apollo3:apollo-rx2-support-java | com.apollographql.java:rx2 | apollographql/apollo-kotlin-java-support |
com.apollographql.apollo3:apollo-rx3-support-java | com.apollographql.java:rx3 | apollographql/apollo-kotlin-java-support |
Those new artifacts also use a new package name. You can usually guess it by removing .apollo3
:
1// Replace
2import com.apollographql.apollo3.mockserver.MockServer
3
4// With
5import com.apollographql.mockserver.MockServer
apollo-runtime
Fetch errors do not throw
execute
, toFlow
and watch
must be updated to the new error handling or changed to their version 3 compat equivalent.See executeV3
and toFlowV3
for temporary methods to help with the migration.See nullability for a quick peek at the future of GraphQL nullability and error handling.In Apollo Kotlin 3, fetch errors like network errors, cache misses, and parsing errors were surfaced by throwing exceptions in ApolloCall.execute()
and in Flows (ApolloCall.toFlow()
, ApolloCall.watch()
).
This was problematic because it was a difference in how to handle GraphQL errors vs other errors. It was also easy to forget catching the exceptions. Uncaught network errors is by far the number one error reported by the Google Play SDK Index. Moreover, throwing terminates a Flow and consumers would have to handle re-collection.
In Apollo Kotlin 4, a new field ApolloResponse.exception
has been added and these errors are now surfaced by returning (for execute()
) or emitting (for Flows) an ApolloResponse
with a non-null exception
instead of throwing it.
Queries and mutations:
1// Replace
2try {
3 val response = client.query(MyQuery()).execute()
4 if (response.hasErrors()) {
5 // Handle GraphQL errors
6 } else {
7 // No errors
8 val data = response.data
9 // ...
10 }
11} catch (e: ApolloException) {
12 // Handle fetch errors
13}
14
15// With
16val response = client.query(MyQuery()).execute()
17if (response.data != null) {
18 // Handle (potentially partial) data
19} else {
20 // Something wrong happened
21 if (response.exception != null) {
22 // Handle fetch errors
23 } else {
24 // Handle GraphQL errors in response.errors
25 }
26}
Subscriptions:
1// Replace
2client.subscription(MySubscription()).toFlow().collect { response ->
3 if (response.hasErrors()) {
4 // Handle GraphQL errors
5 }
6}.catch { e ->
7 // Handle fetch errors
8}
9
10// With
11client.subscription(MySubscription()).toFlow().collect { response ->
12 val data = response.data
13 if (data != null) {
14 // Handle (potentially partial) data
15 } else {
16 // Something wrong happened
17 if (response.exception != null) {
18 // Handle fetch errors
19 } else {
20 // Handle GraphQL errors in response.errors
21 }
22 }
23}
Note that this is true for all Flows
, including watchers. If you don't want to receive error responses (like cache misses), filter them out:
1// Replace
2apolloClient.query(query).watch()
3
4// With
5apolloClient.query(query).watch().filter { it.exception == null }
The lower level ApolloStore
APIs are not changed and throw on cache misses or I/O errors.
ApolloCompositeException is not thrown
When using the cache, Apollo Kotlin 3 throws ApolloCompositeException
if no response is found. For an example, a CacheFirst
fetch policy throws ApolloCompositeException(cacheMissException, apolloNetworkException)
if both cache and network failed.
In those cases, Apollo Kotlin 4 throws the first exception and adds the second as a suppressed exception:
1// Replace
2if (exception is ApolloCompositeException) {
3 val cacheMissException = exception.first
4 val networkException = exception.second
5}
6
7// With
8val cacheMissException = exception
9val networkException = exception.suppressedExceptions.firstOrNull()
For more control over the exception, use toFlow()
and collect the different ApolloResponse
.
emitCacheMisses(Boolean) is removed
In Apollo Kotlin 3, when using the normalized cache, you could set emitCacheMisses(Boolean)
to true
to emit cache misses instead of throwing.
In Apollo Kotlin 4, cache misses always emit a response with response.exception
containing a CacheMissException
. emitCacheMisses(Boolean)
has been removed.
With the CacheFirst
, NetworkFirst
and CacheAndNetwork
policies, cache misses and network errors are now exposed in ApolloResponse.exception
.
Migration helpers
To ease the migration from Apollo Kotlin 3, drop-in helpers functions are provided that restore the version 3 behavior:
ApolloCall.executeV3()
ApolloCall.toFlowV3()
Those helper functions:
throw on fetch errors
make
CacheFirst
,NetworkFirst
andCacheAndNetwork
policies ignore fetch errors.throw ApolloComposite exception if needed.
Because of the number of different options in version 3 and the complexity of error handling, these functions may not 100% match the version 3 behavior, especially in the advanced cases involving watchers. If you are in one of those cases, we strongly recommend using the version 4 functions that are easier to reason about.
Non-standard HTTP headers are not sent by default
X-APOLLO-OPERATION-NAME
and X-APOLLO-OPERATION-ID
are non-standard headers and are not sent by default anymore. If you used them for logging purposes or if you are using Apollo Server CSRF prevention, you can add them back using an ApolloInterceptor:
1val apolloClient = ApolloClient.Builder()
2 .serverUrl(mockServer.url())
3 .addInterceptor(object : ApolloInterceptor {
4 override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
5 return chain.proceed(request.newBuilder()
6 .addHttpHeader("X-APOLLO-OPERATION-NAME", request.operation.name())
7 .addHttpHeader("X-APOLLO-OPERATION-ID", request.operation.id())
8 .build()
9 )
10 }
11 })
12 .build()
ApolloCall.Builder.httpHeaders is additive
In Apollo Kotlin 3, if HTTP headers were set on an ApolloCall
, they would replace the ones set on ApolloClient
. In Apollo Kotlin 4 they are added instead by default. To replace them, call ApolloCall.Builder.ignoreApolloClientHttpHeaders(true)
.
1// Replace
2val call = client.query(MyQuery())
3 .httpHeaders(listOf("key", "value"))
4 .execute()
5
6// With
7val call = client.query(MyQuery())
8 .httpHeaders(listOf("key", "value"))
9 .ignoreApolloClientHttpHeaders(true)
10 .execute()
HttpEngine implements Closeable
HttpEngine
now implements Closeable
and has its dispose
method renamed to close
. If you have a custom HttpEngine
, you need to implement close
instead of dispose
.
apollo-gradle-plugin
Multi-module dependsOn
In Apollo Kotlin 3, depending on an upstream GraphQL module is done using the apolloMetadata
configuration.
In Apollo Kotlin 4, this is now done using the Service.dependsOn()
API. Service.dependsOn()
works for multi-services repositories and, by hiding the configuration names, allows us to change the implementation in the future if needed. This is probably going to be required to accommodate Gradle project isolation.
1// feature1/build.gradle.kts
2
3// Replace
4dependencies {
5 // ...
6
7 // Get the generated schema types (and fragments) from the upstream schema module
8 apolloMetadata(project(":schema"))
9
10 // You also need to declare the schema module as a regular dependency
11 implementation(project(":schema"))
12}
13
14// With
15dependencies {
16 // ...
17
18 // You still need to declare the schema module as a regular dependency
19 implementation(project(":schema"))
20}
21
22apollo {
23 service("service") {
24 // ...
25
26 // Get the generated schema types and fragments from the upstream schema module
27 dependsOn(project(":schema"))
28 }
29}
Auto-detection of used types
In multi-module projects, by default, all the types of an upstream module are generated because there is no way to know in advance what types are going to be used by downstream modules. For large projects this can lead to a lot of unused code and an increased build time.
To avoid this, in Apollo Kotlin 3 you could manually specify which types to generate by using alwaysGenerateTypesMatching
. In Apollo Kotlin 4 you can opt in auto-detection of used types.
To enable this, add the "opposite" link of dependencies with isADependencyOf()
.
1// schema/build.gradle.kts
2
3// Replace
4apollo {
5 service("service") {
6 // ...
7
8 // Generate all the types in the schema module
9 alwaysGenerateTypesMatching.set(listOf(".*"))
10
11 // Enable generation of metadata for use by downstream modules
12 generateApolloMetadata.set(true)
13 }
14}
15
16// With
17apollo {
18 service("service") {
19 // ...
20
21 // Enable generation of metadata for use by downstream modules
22 generateApolloMetadata.set(true)
23
24 // Get used types from the downstream module1
25 isADependencyOf(project(":feature1"))
26
27 // Get used types from the downstream module2
28 isADependencyOf(project(":feature2"))
29
30 // ...
31 }
32}
If you were using apolloUsedCoordinates
, you can also remove it:
1dependencies {
2 // Remove this
3 apolloUsedCoordinates(project(":feature1"))
4}
All schema types are generated in the schema module
Apollo Kotlin 3 allows to generate schema types for enums or input types in any module. While very flexible, this creates a lot of complexity:
Duplicate types could be generated in sibling modules.
It's unclear what package name to use.
Using the downstream module package name means the qualified name of the class changes if the type is moved to another module.