迁移到Apollo Kotlin 4
从第3版本开始
Apollo Kotlin 3是对Apollo在Kotlin多平台上的重大重写。
Apollo Kotlin 4专注于工具、稳定性和使库更容易维护,以便在未来多年内顺利发展。
尽管大多数核心API保持不变,Apollo Kotlin 4包含一些二进制不兼容的变化。为了解决这个问题,并为了更好地面向未来,我们更改了包名 为 com.apollographql.apollo
。请参阅 Apollo Kotlin 演变策略 了解更多详情。
Apollo Kotlin 4 移除了一些已弃用的符号。我们强烈建议更新到最新的 3.x 版本,并在迁移到 4 版本之前移除弃用的用法。
重要变更包括
下面将提供更多详情。
使用 Android Studio/IntelliJ 插件进行自动迁移
Apollo Kotlin 4 版本附带一个 Android Studio/IntelliJ 插件,它自动完成大部分迁移工作。
它自动完成大部分 API 变更,但不能处理如错误处理这样的行为变更。
我们建议使用插件来自动执行重复性任务,但是还是要仔细阅读以下 文档 以了解详细信息。
组 ID/插件 ID/包名
Apollo Kotlin 4 使用新的标识符用于其 Maven 组 ID、Gradle 插件 ID 和包名 : com.apollographql.apollo
。
这也允许在需要时并行运行版本 4 和 3。
在大多数情况下,您可以通过查找和替换操作来更新项目中的标识符,将 com.apollographql.apollo3
替换为 com.apollographql.apollo
。
组 ID
用于识别 Apollo Kotlin 4 的 Maven 组 ID 是 构件,它是 com.apollographql.apollo
:
// Replace:implementation("com.apollographql.apollo3:apollo-runtime:3.8.4")// With:implementation("com.apollographql.apollo:apollo-runtime:4.0.0")
Gradle 插件 ID
Apollo Kotlin 4.0 的 Gradle 插件 ID 是 com.apollographql.apollo
:
// Replace:plugins {id("com.apollographql.apollo3") version "3.8.4"}// With:plugins {id("com.apollographql.apollo") version "4.0.0"}
包名
Apollo Kotlin 4 类使用 com.apollographql.apollo
包:
// Replace:import com.apollographql.apollo3.ApolloClientimport com.apollographql.apollo3.*// With:import com.apollographql.apollo.ApolloClientimport com.apollographql.apollo.*
移动的构件
多年来,许多支持功能被添加到 apollo-api
和 apollo-runtime
构件中。虽然很有用,但其中大部分功能并不具备核心构件同样程度的稳定性和成熟度,将它们捆绑在一起并没有太大意义。
向前发展,只有核心组件保留在 apollo-kotlin
仓库中,并且一起发布。其他组件移动到新的 Maven 坐标和 GitHub 仓库中。
这将使我们能够更快地对新功能进行迭代,同时保持核心组件更小、更易于维护。
新坐标的组件有:
旧坐标 | 新坐标 | 新仓库 |
---|---|---|
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 |
这些新组件也使用了新的包名。您通常可以通过删除 .apollo3
来猜测它:
// Replaceimport com.apollographql.apollo3.mockserver.MockServer// Withimport com.apollographql.mockserver.MockServer
apollo-runtime
捕取错误不会抛出异常
⚠️ 注意:
错误处理更改是编译时未被检测到的行为更改。对 execute
、toFlow
和 watch
的使用必须更新到新的错误处理,或者更改为它们版本 3 的兼容等效版本。
查看executeV3
和 toFlowV3
以获取帮助迁移的临时方法。
查看nullability,快速了解GraphQL中的nullability和错误处理。
在 Apollo Kotlin 3 中,ApolloCall.execute() 和 Flows (ApolloCall.toFlow(), ApolloCall.watch()) 会通过抛出异常来抛出网络错误、缓存未命中和解析错误。
这存在问题,因为它在处理 GraphQL 错误与其他错误时的做法有所不同。也容易忘记捕获异常。未捕获的网络错误是 Google Play SDK Index 上报告的最常见的错误。Google Play SDK Index此外,抛出操作会终止一个 Flow,消费者必须处理重新收集。
在 Apollo Kotlin 4 中,增加了一个新的字段 ApolloResponse.exception,这些错误现在是通过返回(对于 execute())或发出(对于 Flows)一个包含非空异常的 ApolloResponse 来暴露的,而不是抛出。
查询和突变:
// Replacetry {val response = client.query(MyQuery()).execute()if (response.hasErrors()) {// Handle GraphQL errors} else {// No errorsval data = response.data// ...}} catch (e: ApolloException) {// Handle fetch errors}// Withval response = client.query(MyQuery()).execute()if (response.data != null) {// Handle (potentially partial) data} else {// Something wrong happenedif (response.exception != null) {// Handle fetch errors} else {// Handle GraphQL errors in response.errors}}
订阅:
// Replaceclient.subscription(MySubscription()).toFlow().collect { response ->if (response.hasErrors()) {// Handle GraphQL errors}}.catch { e ->// Handle fetch errors}// Withclient.subscription(MySubscription()).toFlow().collect { response ->val data = response.dataif (data != null) {// Handle (potentially partial) data} else {// Something wrong happenedif (response.exception != null) {// Handle fetch errors} else {// Handle GraphQL errors in response.errors}}}
请注意,这对于所有Flows
都成立,包括观察者。如果您不希望接收错误响应(如缓存未命中),请过滤掉它们:
// ReplaceapolloClient.query(query).watch()// WithapolloClient.query(query).watch().filter { it.exception == null }
较低级别的 ApolloStore APIs 没有改变,并在缓存未命中或 I/O 错误时抛出异常。
ApolloCompositeException 不会抛出
当使用缓存时,Apollo Kotlin 3 如果没有找到响应会抛出ApolloCompositeException。例如,如果缓存和网络都失败了,使用CacheFirst
获取策略会抛出ApolloCompositeException(cacheMissException, apolloNetworkException)
在这些情况下,Apollo Kotlin 4 抛出第一个异常,并将第二个异常作为被抑制异常添加:
// Replaceif (exception is ApolloCompositeException) {val cacheMissException = exception.firstval networkException = exception.second}// Withval cacheMissException = exceptionval networkException = exception.suppressedExceptions.firstOrNull()
要更好地控制异常,使用toFlow()`和收集不同的
ApolloResponse
emitCacheMisses(Boolean)
被移除
在 Apollo Kotlin 3 中,当使用规范化缓存时,可以将emitCacheMisses(Boolean)
设置true
以发出缓存未命中而不是抛出。
在 Apollo Kotlin 4 中,缓存未命中始终会发出包含 response.exception
的响应,其中包含一个 CacheMissException
。 emitCacheMisses(Boolean)
已被移除。
使用 CacheFirst
、NetworkFirst
和 CacheAndNetwork
策略时,缓存未命中和网络错误现在会暴露在 ApolloResponse.exception
中。
迁移助手
为了简化从 Apollo Kotlin 3 的迁移,提供了一组即插即用的帮助函数,以恢复 3 版本的行刑:
ApolloCall.executeV3()
ApolloCall.toFlowV3()
这些帮助函数
- 在获取错误时抛出异常
- 使
CacheFirst
、NetworkFirst
和CacheAndNetwork
策略忽略获取错误。 - 如有必要,抛出 ApolloComposite 异常。
由于 3 版本中有许多不同的选项和错误处理复杂性的设置,这些函数可能不完全匹配 3 版本的行为,尤其是在涉及观察者的复杂案例中。如果您处于这些情况之一,我们强烈建议使用 4 版本的函数,这些函数更容易推理。
默认不发送非标准 HTTP 头
X-APOLLO-OPERATION-NAME
和 X-APOLLO-OPERATION-ID
是非标准头,默认不再发送。如果您使用它们进行日志记录或正在使用 Apollo Server CSRF Prevention,您可以使用 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 是可添加的
在 Apollo Kotlin 3 中,如果已在一个 ApolloCall
上设置了 HTTP 头,它们将替换 ApolloClient
上设置的 Http 头。在 Apollo Kotlin 4 中,默认情况下它们是通过添加来设置的。要替换它们,请调用 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 实现 Closeable
HttpEngine
现在实现 Closeable
,并将其 dispose
方法重命名为 close
。如果您有一个自定义 HttpEngine
,您需要实现 close
而不是 dispose
。
apollo-gradle-plugin
多模块依赖关系
在 Apollo Kotlin 3 中,根据上游 GraphQL 模块的依赖,使用的是apolloMetadata
配置。
在 Apollo Kotlin 4 中,现在使用的是Service.dependsOn()
API。Service.dependsOn()
适用于多服务存储库,并且通过隐藏配置名称,可以在必要时更改将来的实现。这可能需要适应Gradle 项目隔离。
// 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"))}}
自动检测使用中的类型
在多模块项目中,默认情况下会生成上游模块的所有类型,因为没有办法提前知道下游模块将使用哪些类型。对于大型项目,这可能导致大量未使用的代码和增加构建时间。
为了避免这种情况,在 Apollo Kotlin 3 中可以通过使用alwaysGenerateTypesMatching
手动指定要生成的类型。在 Apollo Kotlin 4 中,可以选择自动检测使用中的类型。
要启用此功能,请使用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"))// ...}}
如果您之前使用过apolloUsedCoordinates
,也可以将其删除:
dependencies {// Remove thisapolloUsedCoordinates(project(":feature1"))}
所有模式类型都在模式模块中生成
Apollo Kotlin 3 允许在任意模块中生成枚举或输入类型的模式类型。虽然非常灵活,但这也创建了很多复杂性:
- 可能在不同模块中生成重复的类型。
- 不清楚应该使用什么包名。
- 使用下游模块的包名意味着当类型移动到另一个模块时,类的限定名称会发生变化。
- 使用模式模块的包名可能会令人惊讶,因为它不是在 Gradle 配置中指定的。
- Gradle 有 API 以聚合项目中所有项目以实现项目隔离(见gradle#22514)。但对于项目子集来说,目前尚不清楚如何实现这一点(见gradle#29037)。
Apollo Kotlin 4 强制在单个模块中生成所有模式类型,即模式模块。这种限制简化了 Gradle 插件,并使得调试和代码的演进更加容易。
这意味着您无法仅使用类型子集发布您的模式模块,并允许其他模块在另一个存储库中自行生成。如果您正在发布您的模式模块,它需要包含所有消费者使用的模式类型,可以使用alwaysGenerateTypesMatching.set(listOf(".*"))
或手动添加。
如果这个限制太多,请提出您的情况问题,我们将重新审视。
Apollo元数据发布不再自动创建
Apollo Kotlin 3会自动使用 artifactId${project.name}-apollo
创建一个 "apollo" 发布。这导致一些项目中出现了依赖关系解析问题(见apollo-kotlin#4350)。
Apollo Kotlin 4通过暴露组件而不是发布来提供更多关于如何创建发布的控制。要发布Apollo元数据,请手动创建一个MavenPublication
:
publishing {publications {create("apollo", MavenPublication::class.java) {from(components["apollo"])artifactId = "${project.name}-apollo"}}}
自定义标量声明
customScalarsMapping
被移除并替换为mapScalar()
,这使得将其映射到内置类型以及/或为给定类型提供编译器类型适配器变得更加容易:
// ReplacecustomScalarsMapping.set(mapOf("Date" to "kotlinx.datetime.LocalDate"))// WithmapScalar("Date", "kotlinx.datetime.LocalDate")// ReplacecustomScalarsMapping.put("MyLong", "kotlin.Long")// WithmapScalarToKotlinLong("MyLong")
迁移到ApolloCompilerPlugin
Apollo Kotlin 4通过引入ApolloCompilerPlugin
作为定制代码生成的方式。 ApolloCompilerPlugin
替换编译器钩子,并使用ServiceLoader API来加载。结果,与Service.operationIdGenerator
/Service.operationOutputGenerator
以及ApolloCompilerPlugin
一起使用是不可能的。
Service.operationIdGenerator
/Service.operationOutputGenerator
已被弃用,并将在未来版本中删除。您可以按照专门的页面说明迁移到ApolloCompilerPlugin
,并使用ApolloCompilerPlugin.operationIds()
方法:
// 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 compiler plugin module)class MyPlugin: ApolloCompilerPlugin {override fun operationIds(descriptors: List<OperationDescriptor>): List<OperationId>? {return descriptors.map {OperationId(it.source.sha1(), it.name)}}}
操作说明文件的位置
由于Apollo Kotlin现在支持不同的操作说明文件格式,因此operationOutput.json
文件现在在"build/generated/manifest/apollo/$service/operationOutput.json"
生成,而不是"build/generated/operationOutput/apollo/$service/operationOutput.json"
。
useSchemaPackageNameForFragments
已删除
这提供了对 2.x 的兼容性,现在已删除。如果您需要为 片段 而指定的包名,您可以使用 编译器插件 和 ApolloCompilerPlugin.layout()
来代替。
apollo-compiler
"compat" codegenModels
已删除
为了与 2.x 兼容,"compat" 代码生成模型已被提供,现在已删除。 "operationBased"
更加一致并生成更少的代码:
// Replaceapollo {service("service") {codegenModels.set("compat")}}// Withapollo {service("service") {codegenModels.set("operationBased")}}
在生成的模型中,内联 fragment 访问器以 on
作为前缀:
// Replacedata.hero.asDroid// Withdata.hero.onDroid
现在不再需要片段 fragments
伪属性:
// Replacedata.hero.fragments.heroDetails// Withdata.hero.heroDetails
最后,一些来自其父级内联 fragment 合并的 字段 必须现在通过内联片段来访问:
// Replace/*** {* hero {* # this condition is always true* # allowing to merge the name field* ... on Character {* name* }* }* }*/data.hero.name// With/*** name is not merged anymore*/data.hero.onCharacter?.name
ⓘ 注意
Android Studio 插件提供 一个 compat
盘格替换 operationBased
工具,该工具自动执行许多这些更改。
数据构建器
Apollo Kotlin 3 中,使用片段和 需要在 buildFoo {}
块中嵌套字段以确定返回的具体类型。
Apollo Kotlin 4 中,这通过使用 Data()
构造函数的第一个参数来指定。这使得使用片段的数据构建器的语法与操作相同:
// Replaceval data = AnimalDetailsImpl.Data {buildCat {name = "Noushka"species = "Maine Coon"}}// Withval data = AnimalDetailsImpl.Data(Cat) {name = "Noushka"species = "Maine Coon"}
有关更多信息,请参阅 数据构建器文档。
测试构建器已删除
测试构建器是一个实验性功能,已被更简单、与自定义 标度 交互更好的数据构建器所取代。
Apollo Kotlin 4 中,测试构建器不再可用 - 请参阅数据构建器文档以获取更多信息。
枚举类名现在以大写字母开头
为了与其他类型一致,Kotlin 中的 GraphQL 枚举现在需要首字母大写。您可以使用 @targetName
来恢复以前的行为:
# Make sure `someEnum` isn't renamed to `SomeEnum`enum someEnum @targetName(name: "someEnum"){AB}
__Schema
位于 schema
子模块
当使用 generateSchema
选项时,为了避免与同名的 introspection 类型产生名称冲突,现在 __Schema
类型将生成在 schema
子模块中(而不是 type
):
// Replaceimport com.example.type.__Schema// Withimport com.example.schema.__Schema
KotlinLabs 指令现在已升级到 0.3 版本
嵌入式的 kotlin_labs directives 现在已升级到 0.3 版本。
以下是由 Apollo Kotlin 支持的客户端 directives。它们将被自动导入,但如果您因为重命名或其他原因显式依赖于导入,您需要升级版本:
# 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"])
这是一个向后兼容的变更。
指令使用情况得到验证
在 Apollo Kotlin 4 中,所有 directive 使用都会与它们的定义进行验证。如果您有使用客户端指令的查询:
query HeroName {hero {# WARNING: '@required' is not defined by the schemaname @required}}
这些查询现在需要与模式中的匹配 directive 定义。如果您使用 introspection 下载了您的模式,这不应该是一个问题。
在某些情况下,对于客户端 directives 和/或如果没有使用 introspection,指令定义可能缺失。对于这些情况,您可以在一个 extra.graphqls
文件中显式添加它:
directive @required on FIELD
Sealed 类的 UNKNOWN__
构造函数现在是私有的
当使用 sealedClassesForEnumsMatching
选项处理枚举时,现在将 UNKNOWN__
构造函数生成为私有,以防止其意外使用。
建议您更新模式而不是实例化一个 UNKNOWN__
值,但如果您需要,请改用 safeValueOf
方法:
// Replaceval myEnum = MyEnum.UNKNOWN__("foo")// Withval myEnum = MyEnum.safeValueOf("foo")
apollo-normalized-cache
配置顺序
在自动持久化查询之前必须配置规范化缓存,配置在之后将无法成功(见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
在 Apollo Kotlin 3 中,apollo-ast 使用 Antlr 来解析 GraphQL 文档。
在Apollo Kotlin 4 中,apollo-ast 使用其自己的递归下降解析器。这不仅消除了依赖,而且速度更快,还支持 KMP。我们借此机会清理了 apollo-ast API:
- AST 类(
GQLNode
及其子类)以及Introspection
类不再是数据类(见https://github.com/apollographql/apollo-kotlin/pull/4704/)。 - 已经调整了类层次结构,以便
GQLNamed
、GQLDescribed
和GQLHasDirectives
更一致地继承。 GQLSelectionSet
和GQLArguments
已从GQLField
和GQLInlineFragment
中删除。直接使用.selections
。GQLInlineFragment.typeCondition
现在为可为空,以处理继承其类型条件的内部片段。SourceLocation.position
已重命名为SourceLocation.column
,现在是 1 索引。GQLNode.sourceLocation
现在为空可考虑程序性地构造节点的情况。File.toSchema()
和String.toSchema()
已删除。相反,使用toGQLDocument().toSchema()
。
迁移示例
如果您在寻找灵感,我们已更新了版本 3 集成测试以使用版本 4。如果您有一个迁移了的开源项目,请随时分享,我们将将其包含在此处。