Join us for GraphQL Summit, October 10-12 in San Diego. Use promo code ODYSSEY for $400 off your pass.
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: as version 4 is currently under development, this page is a work in progress.

Dependencies / compatibility

  • Apollo Kotlin 4 now requires the Kotlin Gradle Plugin version 1.6+ (whereas 3.x needed 1.5+).
  • Compatibility with non-hierarchical multiplatform projects (enableCompatibilityMetadataVariant) has been removed.

Error handling

Exception surfacing

In v3, non-GraphQL errors like network errors, cache misses, and WebSocket protocol errors were surfaced by throwing exceptions in ApolloCall.execute() and in Flows (ApolloCall.toFlow(), ApolloCall.watch()). This was not ideal 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 v4, a 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.

In addition to this, GraphQL errors are also surfaced in an ApolloGraphQLException set on exception (.errors still exists for convenience).

This allows consumers to handle different kinds of errors at the same place, and it avoids Flows from being terminated.

To ease the migration to v4, the v3 behavior can be restored by calling ApolloClient.Builder.useV3ExceptionHandling(true).

// 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()
val data = response.data
if (data != null) {
// No errors
// ...
} else {
when (response.exception) {
is ApolloGraphQLException -> {
// Handle GraphQL errors
}
else -> {
// Handle network error
}
}
}
// 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) {
// No errors
// ...
} else {
when (response.exception) {
is ApolloGraphQLException -> {
// Handle GraphQL errors
}
else -> {
// Handle network error
}
}
}
}

Exception folding when using the cache

In v3, when using the normalized cache, you could set emitCacheMisses to true to emit cache misses instead of throwing.

In v4, 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.

To ease the migration to v4, the v3 behavior can be restored by calling ApolloClient.Builder.useV3ExceptionHandling(true).

Exceptions in watch

In v3, ApolloCall.watch() would accept a fetchThrows and refetchThrows parameters to control whether to throw cache or network exceptions happening during the initial fetch or subsequent refetches, the default value being false.

In v4, these exceptions are now emitted in ApolloResponse.exception.

If they are undesirable, ignore them by filtering them out:

client.query(MyQuery()).watch().filter { it.exception == null }.collect { response ->
// Handle responses
}

HTTP headers

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()

Multi-module improvements

In a multi-module setup, the syntax for depending on an upstream module (e.g. the module) has changed: use dependsOn in the apollo block instead of apolloMetadata in the dependencies block.

// 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
apollo {
service("service") {
// ...
// Enable generation of metadata for use by downstream modules
generateApolloMetadata.set(true)
// Get used types from the downstream module
isADependencyOf(project(":feature1"))
// You can have several downstream modules
isADependencyOf(project(":feature2"))
}
}

Code generation

  • Enum class names now have their first letter capitalized, as other generated types.
  • 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.
  • addTypename = "ifFragments" is deprecated as it could lead to cache misses for cache users:
{
node(id: 42) {
# no fragment here means but we if we don't query __typename, the query below will emit a cache miss
title
price
}
}
{
product(id: 42) {
# __typename is added here automatically
__typename
... on Product {
title
price
}
}
}

Instead, use "always" if you're using the cache or "ifPolymorphic" else. See #3965 for more details.

Cache

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 s 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 with toSchema().

Gradle configuration

  • Publishing is no longer configured automatically.
  • Because Apollo Kotlin now supports different manifest formats, operationOutput.json has moved from "build/generated/operationOutput/apollo/$service/operationOutput.json" to "build/generated/manifest/apollo/$service/operationOutput.json"
Previous
Get Started
Next
0. Introduction
Edit on GitHubEditForumsDiscord