Migrating to Apollo Kotlin 4
From version 3
ⓘ NOTE
Version 4 is currently under development and this page itself is a work in progress.
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
.
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.
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
:
// Replace:implementation("com.apollographql.apollo3:apollo-runtime:3.8.4")// With:implementation("com.apollographql.apollo:apollo-runtime:4.0.0")
Gradle plugin id
The Apollo Kotlin 4.0 Gradle plugin id is com.apollographql.apollo
:
// Replace:plugins {id("com.apollographql.apollo3") version "3.8.4"}// With:plugins {id("com.apollographql.apollo") version "4.0.0"}
Package name
Apollo Kotlin 4 classes use the com.apollographql.apollo
package:
// Replace:import com.apollographql.apollo3.ApolloClientimport com.apollographql.apollo3.*// With:import com.apollographql.apollo.ApolloClientimport 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-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 have a new package name. You can usually guess it by removing .apollo3
:
// Replaceimport com.apollographql.apollo3.mockserver.MockServer// Withimport com.apollographql.mockserver.MockServer
apollo-runtime
Fetch errors do not throw
⚠️ CAUTION
Error handling changes are a behavior change that is not detected at compile time. Usages of 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, 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 and it was 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.
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
When using the cache, Apollo Kotlin 3 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 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()
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
to true
to emit cache misses instead of throwing.
In Apollo Kotlin 4, 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 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:
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()
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)
.
// Replaceval call = client.query(MyQuery()).httpHeaders(listOf("key", "value")).execute()// Withval call = client.query(MyQuery()).httpHeaders(listOf("key", "value")).ignoreApolloClientHttpHeaders(true).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, 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()
. Service.dependsOn()
works for multi-services repositories and, by hiding the configuration names, allows us to change the implementation in the future if needed. For an example to accommodate Gradle project isolation.
// 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 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()
.
// 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"))}
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.
- Using the schema module package name can be surprising as it's not the one specified in the Gradle configuration.
- Gradle has APIs to aggregate all projects in a project isolation compatible way (see gradle#22514). But it's not clear yet how to do this for a subset of the projects (see gradle#29037).
In Apollo Kotlin 4, we decided to enforce generating all schema types in a single module, the schema module. This restriction simplifies the Gradle plugin and makes it easier to debug and evolve that code.
It also means that you can't publish your schema module with a subset of the types and let other modules generate their own in another repository. If you are publishing your schema module, it needs to contain all the schema types used by your consumers, either using alwaysGenerateTypesMatching.set(listOf(".*"))
or manually adding them.
If that is too much of a limitation, please open an issue and we will revisit.
Apollo metadata publications are no longer created automatically
Apollo Kotlin 3 automatically creates an "apollo" publication using artifactId ${project.name}-apollo
. This created dependency resolution issues in some projects (see apollo-kotlin#4350).
Apollo Kotlin 4 exposes a software component instead of a publication to give more control over how publications are created. In order to publish the Apollo metadata, create a MavenPublication
manually:
publishing {publications {create("apollo", MavenPublication::class.java) {from(components["apollo"])artifactId = "${project.name}-apollo"}}}
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.
Migrating to ApolloCompilerPlugin
Apollo Kotlin 4 introduces ApolloCompilerPlugin
as a way to customize code generation. ApolloCompilerPlugin
are loaded using the ServiceLoader API and run in a separate classloader from your Gradle build. As a result, using Service.operationIdGenerator
/Service.operationOutputGenerator
together with ApolloCompilerPlugin
is not possible.
Service.operationIdGenerator
/Service.operationOutputGenerator
are deprecated and will be removed in a future release. You can migrate to ApolloCompilerPlugin
using the instructions from the dedicated page and the ApolloCompilerPlugin.operationIds()
method:
// Replace (in your build scripts)val operationOutputGenerator = object: OperationOutputGenerator {override fun generate(operationDescriptorList: Collection<OperationDescriptor>): OperationOutput {return operationDescriptorList.associateBy {it.source.sha1()}}override val version: String = "v1"}// Or, if using OperationIdGenerator, replaceval operationIdGenerator = object: OperationIdGenerator {override fun apply(operationDocument: String, operationName: String): String {return operationDocument.sha1()}override val version: String = "v1"}// With (in your build compiler plugin module)class MyPlugin: ApolloCompilerPlugin {override fun operationIds(descriptors: List<OperationDescriptor>): List<OperationId>? {return descriptors.map {OperationId(it.source.sha1(), it.name)}}}
Operation manifest file location
Since Apollo Kotlin now supports different operation manifest formats, the operationOutput.json
file will be generated in "build/generated/manifest/apollo/$service/operationOutput.json"
instead of "build/generated/operationOutput/apollo/$service/operationOutput.json"
.
useSchemaPackageNameForFragments
is removed
This was provided for compatibility with 2.x and is now removed. If you need specific package names for fragments, you can use a compiler plugin and ApolloCompilerPlugin.layout()
instead.
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("operationBased")}}
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
ⓘ NOTE
The Android Studio plugin provides a compat
to operationBased
migration tool which automates a lot of these changes.
Test builders are removed
Test builders were an experimental feature that have been superseded by data builders, a simpler version that also plays nicer with custom scalars.
In Apollo Kotlin 4, test builders are no longer available - please refer to the data builders documentation for more information.
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 behavior using @targetName
:
# Make sure `someEnum` isn't renamed to `SomeEnum`enum someEnum @targetName(name: "someEnum"){AB}
__Schema
is in the schema
subpackage
When using the generateSchema
option, 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
):
// Replaceimport com.example.type.__Schema// Withimport data.builders.schema.__Schema
KotlinLabs directives are now at version 0.3
The embedded kotlin_labs directives are now at version 0.3.
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.3/", import: ["@typePolicy"])
This is a backward compatible change.
Directive usages are validated and require a matching directive definition
In Apollo Kotlin 4, all directive usages are validated against their definition. If you have queries using client side directives:
query HeroName {hero {# WARNING: '@required' is not defined by the schemaname @required}}
Those queries now required to have a matching directive definition in the schema. If the directive is a server directive, and you downloaded your schema using introspection, the directive definition should be present.
In some cases, for client directives and/or if you did not use introspection, the directive definition might be missing. For those cases, you can add it explicitely in a extra.graphqls
file:
directive @required on FIELD
Sealed class UNKNOWN__
constructors are private
When generating enums as sealed classes with the sealedClassesForEnumsMatching
option, the UNKNOWN__
constructor is now generated as private, to prevent its accidental usage.
It is recommended to update your schema instead of instantiating an UNKNOWN__
value, but if you need to, use the safeValueOf
method instead:
// Replaceval myEnum = MyEnum.UNKNOWN__("foo")// Withval myEnum = MyEnum.safeValueOf("foo")
apollo-normalized-cache
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).
// Replaceval apolloClient = ApolloClient.Builder().serverUrl(...).autoPersistedQueries(...).normalizedCache(...).build()// Withval apolloClient = ApolloClient.Builder().serverUrl(...).normalizedCache(...).autoPersistedQueries(...).build()
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 to 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()
.
apollo-rx{2-3}-support
The apollo-rx2-support
and apollo-rx3-support
artifacts are deprecated.
They were very thin wrappers around kotlinx-coroutines-rx2
or kotlinx-coroutines-rx3
and will be removed in a future version.
Use kotlinx-coroutines-rx${version}
directlyl 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()
apollo-idling-resource
ApolloIdlingResource is deprecated. We recommend using reactive patterns to test your UI instead. See this article about ways to do so for more details.
Example of a migration
If you are looking for inspiration, we updated the version 3 integration tests to use version 4. If you have an open source project that migrated, feel free to share it and we'll include it here.