基于响应的代码生成
Apollo Kotlin 尝试处理您的 GraphQL 操作,为其生成Kotlin模型,并在您的JSON响应中实例化它们,允许您以类型安全的方式访问您的数据。
实际上有3个不同的领域在起作用
- The GraphQL 领域: 操作s
- The Kotlin domain: models
- The JSON domain: responses
默认情况下, Apollo Kotlin 生成的模型与您的 GraphQL 操作 一一对应。内联和命名的 片段 生成合成 字段,因此您可以使用Kotlin代码data.hero.onDroid.primaryFunction
. 片段 是可以在不同 operations 中重用的类。这个代码生成引擎codegen被称为 operationBased
,因为它与GraphQL operation 相匹配。
JSON响应的形状可能与您的 GraphQL 操作不同。这是使用合并字段或片段时的情况。如果您想以JSON响应中的样子访问Kotlin属性,Apollo Kotlin提供了与JSON响应一一对应的responseBased
代码生成器。GraphQL片段以Kotlin接口的形式表示,因此您可以使用Kotlin代码访问其字段,例如(data.hero as Droid).primaryFunction
。因为它们映射到JSON响应,responseBased
模型具有允许JSON流式传输和/或映射到动态JS对象的属性。但鉴于GraphQL是一种非常灵活的语言,创建一个生成非常大的JSON响应的GraphQL查询也是很容易的。
因此以及其他限制,我们建议默认使用operationBased
代码生成器。
此页首先回顾operationBased
代码生成器的工作原理,然后再解释responseBased
代码生成器。最后,列出了使用responseBased
代码生成器的不同限制,以便您在使用此代码生成器时做出明智的决定。
要使用特定的代码生成器,请在您的Gradle脚本中配置codegenModels
:
apollo {service("service") {// ...codegenModels.set("responseBased")}}
默认的operationBased
The operationBased
代码生成器根据操作的形状生成模型。
- 为每个复合字段选择生成一个模型。
- 片段和内联片段作为自己的类生成。
- 合并字段在查询时存储多次。
例如,给定这个查询:
query HeroForEpisode($ep: Episode!) {search {hero(episode: $ep) {name... on Droid {nameprimaryFunction}...HumanFields}}}fragment HumanFields on Human {height}
代码生成器生成以下类
class Search(val hero: Hero?)class Hero(val name: String,val onDroid: OnDroid?,val humanFields: HumanFields?)class OnDroid(val name: String,val primaryFunction: String)
class HumanFields(val height: Double)
注意onDroid
和 humanFields
在 Hero
类中是可空的。这是因为它们是否存在取决于返回的超级英雄的具体类型:
val hero = data.search?.herowhen {hero.onDroid != null -> {// Hero is a Droidprintln(hero.onDroid.primaryFunction)}hero.humanFields != null -> {// Hero is a Humanprintln(hero.humanFields.height)}else -> {// Hero is something elseprintln(hero.name)}}
基于响应的代码生成器
与基于操作的代码生成器相比,基于响应的代码生成器有以下不同:
- 生成的模型与接收在操作响应中的JSON结构的 1:1's 结构进行映射。
- 多态通过生成 接口 处理。可能的形状定义为实现相应接口的不同类。
- 片段也作为 接口 生成。
- 任何合并的字段在生成模型中只出现 一次。
让我们通过使用片段的示例来突出一些这些差异。
内联片段
考虑这个 查询:
query HeroForEpisode($ep: Episode!) {hero(episode: $ep) {name... on Droid {primaryFunction}... on Human {height}}}
如果在操作上运行基于响应的代码生成器,它会生成一个具有三个实现类的 Hero
接口:
DroidHero
HumanHero
OtherHero
由于 Hero
是具有不同实现接口,可以使用 when
表达式来处理每个不同的情况:
when (hero) {is DroidHero -> println(hero.primaryFunction)is HumanHero -> println(hero.height)else -> {// Account for other Hero types (including unknown ones)// Note: in this example `name` is common to all Hero typesprintln(hero.name)}}
访问器
作为便利性,基于响应的代码生成器生成命名模式为 as<ShapeName>
的方法(例如,asDroid
或 asHuman
),这使您无需手动转换:
val primaryFunction = hero1.asDroid().primaryFunctionval height = hero2.asHuman().height
命名片段
考虑这个示例
query HeroForEpisode($ep: Episode!) {hero(episode: $ep) {name...DroidFields...HumanFields}}fragment DroidFields on Droid {primaryFunction}fragment HumanFields on Human {height}
基于响应的代码生成器为 DroidFields
和 HumanFields
片段生成接口:
interface DroidFields {val primaryFunction: String}interface HumanFields {val height: Double}
这些接口是由生成的 HeroForEpisodeQuery.Data.Hero
子类(及其他使用这些片段的任何操作模型)实现的:
interface Hero {val name: String}data class DroidHero(override val name: String,override val primaryFunction: String) : Hero, DroidFieldsdata class HumanHero(override val name: String,override val height: Double) : Hero, HumanFieldsdata class OtherHero(override val name: String) : Hero
可以这样使用
when (hero) {is DroidFields -> println(hero.primaryFunction)is HumanFields -> println(hero.height)}
访问器
为了方便,responseBased
代码生成器会生成以模式命名的方法,即 <fragmentName>
(例如,对于名为 fragment 的 DroidFields
,其方法名为 droidFields
)。这样您可以将调用链起来,如下所示:
val primaryFunction = hero1.droidFields().primaryFunctionval height = hero2.humanFields().height
responseBased
代码生成的限制
- 由于 GraphQL 是一种非常具有表现力的语言,因此很容易创建生成非常大的 JSON 响应的 GraphQL 查询。如果使用大量嵌套 fragments,生成的代码大小会随着嵌套层次的增加而指数级增长。我们已知一些相对较小的 GraphQL 查询会超过 JVM 的限制,如 最大方法大小。
- 当使用 fragments 时,必须为使用 fragments 的每个操作生成数据类。为了避免名称冲突,模型是嵌套的,这带来两个副作用:
- 生成的
.class
文件名可能会非常长,这在 MacOS 上会打破默认的 256 个字符的最大文件名限制 - 类似的接口可能会嵌套(用于 fragments)。虽然这在 Kotlin 中是有效的,但 Java 不允许这样做,如果使用它,会导致 kapt 错误。
- 生成的
@include
,@skip
和@defer
指令不被 fragments 在 responseBased 代码生成器上支持。支持它们需要在每次使用这些指令时生成两倍的模型。