基于响应的代码生成
Apollo Kotlin 从您的 GraphQL 操作中获取操作,为它们生成相应的Kotlin模型,并从您的JSON响应中实例化它们,使您能够以类型安全的方式访问数据。
实际上有3个不同的域在进行
- GraphQL域: 操作
- Kotlin域:模型
- JSON域:响应
默认情况下, Apollo Kotlin 生成的模型与您的 GraphQL 操作完全匹配。内联和命名的 片段 生成合成 字段,因此您可以使用Kotlin代码访问GraphQL片段,例如 data.hero.onDroid.primaryFunction
。 片段 是可以跨不同 操作 重用的类。这个代码生成引擎(codegen)称为 operationBased
,因为它与GraphQL操作匹配。
尽管如此,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映射。
- 多态性通过生成 接口 来处理。可能的形状被定义为实现相应接口的不同类。
- 片段也作为 接口 生成。
- 任何合并 字段 仅在生成的模型中 出现一次 。
让我们通过使用片段的例子来突出这些差异。
内联片段
考虑这个 query
:
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 是一门非常 expressive 的语言,因此很容易创建一个生成非常大的 JSON 响应的 GraphQL 查询。如果使用大量的嵌套 fragments,生成的代码大小将与嵌套层级成指数级增长。我们曾看到相对较小的 GraphQL 查询破坏了 JVM 的限制,如 最大方法大小。
- 当使用 fragments 时,必须为每个使用 fragments 的操作生成数据类。为了避免名称冲突,模型是嵌套的,这会带来两个副作用:
- 生成的
.class
文件名可能非常长,会超出 macOS 的默认最大文件名长度 256。 - 相同名称的接口可能会嵌套(针对 fragments)。虽然在 Kotlin 中这是有效的,但是 Java 不允许这样做,并且如果使用 kapt,则会导致编译失败。
- 生成的
@include
、@skip
和@defer
指令在 responseBased 代码生成器中不支持 fragments。支持这些指令会在使用这些指令时生成两倍数量的模型。