Migrating to Apollo Kotlin 4.0
From 3.x
ⓘ NOTE
Version 4 is currently under development and this page itself is a work in progress.Apollo Kotlin 3.0 was a major rewrite of Apollo in Kotlin multiplatform.
Apollo Kotlin 4.0 focuses on tooling, stability and fixing some API regrets that came with 3.x.
Because most of the common APIs stayed the same, we kept the package name unchanged. Apollo Kotlin 4.0 removes some deprecated symbols. We strongly recommend removing deprecated usages before migrating to 4.0.
If you are using a lib that depends on Apollo Kotlin transitively, you need it to update to 4.x before you can update your own app to 4.0.
Automatic migration using the Android Studio plugin
Apollo Kotlin 4 ships with a companion Android Studio plugin that automates most of the migration.
It automates most of the API replacements but cannot deal with behaviour changes like error handling.
We recommend using the plugin to automate the repetitive tasks but still go through this document for the details.
Error handling
⚠️ Error handling changes are a behaviour change that is not be detected at compile time. Usages of execute
, toFlow
and watch
must be updated to the new error handling or changed to their v3 compat equivalent.
Fetch errors do not throw
In Apollo Kotlin 3.x, non-GraphQL 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. Moreover, throwing terminates a Flow and consumers would have to handle re-collection.
In Apollo Kotlin 4.0, 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.
This allows consumers to handle different kinds of errors at the same place, and it prevents Flows from being terminated.
Queries and mutations:
// Replacetry {val response = client.query(MyQuery()).execute()if (response.hasErrors()) {// Handle GraphQL errors} else {// No errorsval data = response.data// ...}} catch (e: ApolloException) {// Handle network error}// Withval response = client.query(MyQuery()).execute()if (response.data != null) {// Handle (potentially partial) data} else {// Something wrong happenedif (response.exception != null) {// Handle non-GraphQL errors} else {// Handle GraphQL errors in response.errors}}
Subscriptions:
// Replaceclient.subscription(MySubscription()).toFlow().collect { response ->if (response.hasErrors()) {// Handle GraphQL errors}}.catch { e ->// Handle network error}// Withclient.subscription(MySubscription()).toFlow().collect { response ->val data = response.dataif (data != null) {// Handle (potentially partial) data} else {// Something wrong happenedif (response.exception != null) {// Handle non-GraphQL errors} else {// Handle GraphQL errors in response.errors}}}
Note that this is true for all Flows
, including watchers. If you don't want to receive error responses, filter them out:
// ReplaceapolloClient.query(query).watch()// WithapolloClient.query(query).watch().filter { it.exception == null }
ApolloCompositeException is not thrown anymore
When using the cache, Apollo Kotlin 3.x threw ApolloCompositeException
if no response could be found. For an example, a CacheFirst
fetch policy would throw ApolloCompositeException(cacheMissException, apolloNetworkException
if both cache and network failed.
In those cases, Apollo Kotlin 4.0 throws the first exception and adds the second as a suppressed exception:
// Replaceif (exception is ApolloCompositeException) {val cacheMissException = exception.firstval networkException = exception.second}// Withval cacheMissException = exceptionval networkException = exception.suppressedExceptions.firstOrNull()
emitCacheMisses(Boolean) is removed
In Apollo Kotlin 3.x, when using the normalized cache, you could set emitCacheMisses
to true
to emit cache misses instead of throwing.
In Apollo Kotlin 4.0, this is now the default behavior and emitCacheMisses
has been removed.
With the CacheFirst
, NetworkFirst
and CacheAndNetwork
policies, cache misses and network errors are now emitted in ApolloResponse.exception
.
Migration helpers
To ease the migration from 3.x, drop-in helpers functions are provided that restore the V3 behaviour:
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 3.x and the complexity of error handling, these functions do not pretend to match 1:1 the V3 behaviour, especially in the advanced cases involving watchers. If you are in one of those cases, we strongly recommend using the 4.0 functions that are easier to reason about.
Other Apollo Runtime changes
HTTP headers
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:
val apolloClient = ApolloClient.Builder().serverUrl(mockServer.url()).addInterceptor(object : ApolloInterceptor {override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {return chain.proceed(request.newBuilder().addHttpHeader("X-APOLLO-OPERATION-NAME", request.operation.name()).addHttpHeader("X-APOLLO-OPERATION-ID", request.operation.id()).build())}}).build()
In v3, if HTTP headers were set on an ApolloCall
, they would replace the ones set on ApolloClient
. In v4 they are added instead by default. To replace them, call ApolloCall.Builder.ignoreApolloClientHttpHeaders(true)
.
// Replaceval call = client.query(MyQuery()).httpHeaders(listOf("key", "value")).execute()// Withval call = client.query(MyQuery()).httpHeaders(listOf("key", "value")).ignoreApolloClientHttpHeaders(true).execute()
HttpEngine
now 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, you could depend on an upstream GraphQL module by using the apolloMetadata
configuration.
In Apollo Kotlin 4, this is now done with the Service.dependsOn()
. This allows better management of dependencies when multiple services are used as better symmetry with isADependencyOf
below.
// feature1/build.gradle.kts// Replacedependencies {// ...// Get the generated schema types (and fragments) from the upstream schema moduleapolloMetadata(project(":schema"))// You also need to declare the schema module as a regular dependencyimplementation(project(":schema"))}// Withdependencies {// ...// You still need to declare the schema module as a regular dependencyimplementation(project(":schema"))}apollo {service("service") {// ...// Get the generated schema types (and fragments) from the upstream schema moduledependsOn(project(":schema"))}}
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 v3 you could manually specify which types to generate by using alwaysGenerateTypesMatching
. In v4 this can now be computed automatically by detecting which types are used by the downstream modules.
To enable this, add the "opposite" link of dependencies with isADependencyOf()
.
// schema/build.gradle.kts// Replaceapollo {service("service") {// ...// Generate all the types in the schema modulealwaysGenerateTypesMatching.set(listOf(".*"))// Enable generation of metadata for use by downstream modulesgenerateApolloMetadata.set(true)}}// Withapollo {service("service") {// ...// Enable generation of metadata for use by downstream modulesgenerateApolloMetadata.set(true)// Get used types from the downstream module1isADependencyOf(project(":feature1"))// Get used types from the downstream module2isADependencyOf(project(":feature2"))// ...}}
If you were using apolloUsedCoordinates
, you can also remove it:
dependencies {// Remove thisapolloUsedCoordinates(project(":feature1"))}
Custom scalars declaration
customScalarsMapping
is removed and replaced with mapScalar()
which makes it easier to map to built-in types and/or provide a compile type adapter for a given type:
// ReplacecustomScalarsMapping.set(mapOf("Date" to "kotlinx.datetime.LocalDate"))// WithmapScalar("Date", "kotlinx.datetime.LocalDate")// ReplacecustomScalarsMapping.put("MyLong", "kotlin.Long")// WithmapScalarToKotlinLong("MyLong")
schemaFile is deprecated
Because there might be several schema files, schemaFile
is deprecated. Instead use schemaFiles
:
// replaceschemaFile.set("src/main/graphql/com/example/schema.graphqls")// withschemaFiles.from("src/main/graphql/com/example/schema.graphqls")
If you are using packageNamesFromFilePaths
and schemaFile
, you'll need to use a Gradle FileTree to carry the appropriate normalized path.
packageNameFromFilePaths()// replaceschemaFile.set("src/main/graphql/com/example/schema.graphqls")// withschemaFiles.from(fileTree("src/main/graphql/").apply {include("com/example/schema.graphqls")})
ⓘ NOTE
Apollo Kotlin 3 was using the operation root directories to compute the schema normalized path which could be wrong in some edge cases. Using fileTree ensures the normalized path is consistent.Misc
- Publishing is no longer configured automatically.
- Because Apollo Kotlin now supports different operation manifest formats,
operationOutput.json
has moved from"build/generated/operationOutput/apollo/$service/operationOutput.json"
to"build/generated/manifest/apollo/$service/operationOutput.json"
- useSchemaPackageNameForFragments is removed
Apollo Compiler
"compat" codegenModels
is removed
The "compat"
codegen models was provided for compatibility with 2.x and is now removed. "operationBased"
is more consistent and generates less code:
// Replaceapollo {service("service") {codegenModels.set("compat")}}// Withapollo {service("service") {codegenModels.set("codegenModels")}}
In the generated models, inline fragments accessors are prefixed with on
:
// Replacedata.hero.asDroid// Withdata.hero.onDroid
The fragments
synthetic property is not needed anymore:
// Replacedata.hero.fragments.heroDetails// Withdata.hero.heroDetails
Finally some fields that were merged from inline fragments in their parents must now be accessed through the inline fragment:
// Replace/*** {* hero {* # this condition is always true* # allowing to merge the name field* ... on Character {* name* }* }* }*/data.hero.name// With/*** name is not marged anymore*/data.hero.onCharacter?.name
The Android Studio plugin provides a compat to operationBased migration tool which automates a lot of these changes.
Enum class names now have their first letter capitalized
For consistency with other types, GraphQL enums are now capitalized in Kotlin. You can restore the previous behaviour using @targetName
:
# Make sure `someEnum` isn't renamed to `SomeEnum`enum someEnum @targetName(name: "someEnum"){AB}
__Schema
is in the schema
subpackage
If using the generateSchema
option, and to avoid a name clash with the introspection type of the same name, the __Schema
type is now generated in a schema
subpackage (instead of type
) when using the generateSchema
option:
// Replaceimport com.example.type.__Schema// Withimport data.builders.schema.__Schema
KotlinLabs directives are now version 0.2
The embedded kotlinlabs/0.2 directives are now version 0.2.
These are the client directives supported by Apollo Kotlin. They are imported automatically but if you relied on an explicit import for renames or another reason, you'll need to bump the version:
# Replaceextend schema @link(url: "https://specs.apollo.dev/kotlin_labs/v0.1/", import: ["@typePolicy"])# Withextend schema @link(url: "https://specs.apollo.dev/kotlin_labs/v0.2/", import: ["@typePolicy"])
This is a backward compatible change.
Cache
ApolloStore
In Apollo Kotlin 3.x, most ApolloStore
functions were marked as suspend
even though they were not actually suspending and perform the work in the thread they are called from.
In particular, they can be blocking when the underlying cache is doing IO, so calling them from the main thread can lead to ANRs.
In Apollo Kotlin 4.0 this is still the case but the functions are no longer marked as suspend
to avoid any confusion.
Configuration order
The normalized cache must be configured before the auto persisted queries, configuring it after will now fail (see https://github.com/apollographql/apollo-kotlin/pull/4709).
apollo-ast
The AST classes (GQLNode
and subclasses) as well as Introspection
classes are not data classes anymore (see https://github.com/apollographql/apollo-kotlin/pull/4704/). The class hierarchy has been tweaked so that GQLNamed
, GQLDescribed
and GQLHasDirectives
are more consistently inherited from.
GQLSelectionSet
and GQLArguments
are deprecated and removed from GQLField
and GQLInlineFragment
. Use .selections
directly
GQLInlineFragment.typeCondition
is now nullable to account for inline fragments who inherit their type condition.
SourceLocation.position
is renamed SourceLocation.column
and is now 1-indexed. GQLNode.sourceLocation
is now nullable to account for the cases where the nodes are constructed programmatically.
It is not possible to create a Schema
from a File or String directly anymore. Instead, create a GQLDocument
first and convert it to a schema with toSchema()
.
Using RxJava artifacts is now an error
The apollo-rx2-support
and apollo-rx3-support
artifacts are very thin wrappers around kotlinx-coroutines-rx2
or kotlinx-coroutines-rx3
and will be removed in a future version.
In Apollo Kotlin 4, use kotlinx-coroutines-rx${version}
instead. Replace the artifact in your build script:
dependencies {// Replaceimplementation("com.apollographql.apollo3:apollo-rx2-support:$version")// Withimplementation("org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$version")}
In your code, use asFLowable()
:
// ReplaceapolloClient.query().rxFlowable()// WithapolloClient.query().toFlow().asFlowable()// ReplaceapolloClient.query().rxSingle()// WithapolloClient.query().toFlow().asFlowable().firstOrError()
Example of a migration
If you are looking for inspiration, we updated the 3.x integration tests to use 4.0. If you have an open source project that migrated, feel free to share it and we'll include it here.