Here's your chance to speak at GraphQL Summit in New York City, October 8 - 10, 2024! 🏙️ Submit your proposal by May 31.
Docs
Launch GraphOS Studio
You're viewing documentation for an upcoming version of this software. Switch to the latest stable version.

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.

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 for the details.

Error handling

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

// Replace
try {
val response = client.query(MyQuery()).execute()
if (response.hasErrors()) {
// Handle GraphQL errors
} else {
// No errors
val data = response.data
// ...
}
} catch (e: ApolloException) {
// Handle network error
}
// With
val response = client.query(MyQuery()).execute()
if (response.data != null) {
// Handle (potentially partial) data
} else {
// Something wrong happened
if (response.exception != null) {
// Handle non-GraphQL errors
} else {
// Handle GraphQL errors in response.errors
}
}

:

// Replace
client.subscription(MySubscription()).toFlow().collect { response ->
if (response.hasErrors()) {
// Handle GraphQL errors
}
}.catch { e ->
// Handle network error
}
// With
client.subscription(MySubscription()).toFlow().collect { response ->
val data = response.data
if (data != null) {
// Handle (potentially partial) data
} else {
// Something wrong happened
if (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:

// Replace
apolloClient.query(query).watch()
// With
apolloClient.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:

// Replace
if (exception is ApolloCompositeException) {
val cacheMissException = exception.first
val networkException = exception.second
}
// With
val cacheMissException = exception
val 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 and CacheAndNetwork 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 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).

// Replace
val call = client.query(MyQuery())
.httpHeaders(listOf("key", "value"))
.execute()
// With
val 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
// Replace
dependencies {
// ...
// Get the generated schema types (and fragments) from the upstream schema module
apolloMetadata(project(":schema"))
// You also need to declare the schema module as a regular dependency
implementation(project(":schema"))
}
// With
dependencies {
// ...
// You still need to declare the schema module as a regular dependency
implementation(project(":schema"))
}
apollo {
service("service") {
// ...
// Get the generated schema types (and fragments) from the upstream schema module
dependsOn(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
// Replace
apollo {
service("service") {
// ...
// Generate all the types in the schema module
alwaysGenerateTypesMatching.set(listOf(".*"))
// Enable generation of metadata for use by downstream modules
generateApolloMetadata.set(true)
}
}
// With
apollo {
service("service") {
// ...
// Enable generation of metadata for use by downstream modules
generateApolloMetadata.set(true)
// Get used types from the downstream module1
isADependencyOf(project(":feature1"))
// Get used types from the downstream module2
isADependencyOf(project(":feature2"))
// ...
}
}

If you were using apolloUsedCoordinates, you can also remove it:

dependencies {
// Remove this
apolloUsedCoordinates(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:

// Replace
customScalarsMapping.set(mapOf(
"Date" to "kotlinx.datetime.LocalDate"
))
// With
mapScalar("Date", "kotlinx.datetime.LocalDate")
// Replace
customScalarsMapping.put("MyLong", "kotlin.Long")
// With
mapScalarToKotlinLong("MyLong")

schemaFile is deprecated

Because there might be several schema files, schemaFile is deprecated. Instead use schemaFiles:

// replace
schemaFile.set("src/main/graphql/com/example/schema.graphqls")
// with
schemaFiles.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()
// replace
schemaFile.set("src/main/graphql/com/example/schema.graphqls")
// with
schemaFiles.from(fileTree("src/main/graphql/").apply {
include("com/example/schema.graphqls")
})

NOTE

Apollo Kotlin 3 was using the 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

4.0 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, replace
val 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)
}
}
}

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"
  • useSchemaPackageNameFor 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:

// Replace
apollo {
service("service") {
codegenModels.set("compat")
}
}
// With
apollo {
service("service") {
codegenModels.set("codegenModels")
}
}

In the generated models, inline accessors are prefixed with on:

// Replace
data.hero.asDroid
// With
data.hero.onDroid

The fragments synthetic property is not needed anymore:

// Replace
data.hero.fragments.heroDetails
// With
data.hero.heroDetails

Finally some 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"){
A
B
}

__Schema is in the schema subpackage

If using the generateSchema option, and to avoid a name clash with the type of the same name, the __Schema type is now generated in a schema subpackage (instead of type) when using the generateSchema option:

// Replace
import com.example.type.__Schema
// With
import data.builders.schema.__Schema

KotlinLabs directives are now version 0.3

The embedded kotlin_labs are now 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:

# Replace
extend schema @link(url: "https://specs.apollo.dev/kotlin_labs/v0.1/", import: ["@typePolicy"])
# With
extend 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 4, all usages are validated against their definition. If you have queries using client side directives:

query HeroName {
hero {
# WARNING: '@required' is not defined by the schema
name @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 n ot 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

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 , 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 {
// Replace
implementation("com.apollographql.apollo3:apollo-rx2-support:$version")
// With
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$version")
}

In your code, use asFLowable():

// Replace
apolloClient.query().rxFlowable()
// With
apolloClient.query().toFlow().asFlowable()
// Replace
apolloClient.query().rxSingle()
// With
apolloClient.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.

Previous
Get Started
Next
0. Introduction
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company