Slide 1

Slide 1 text

What’s new in Apollo Kotlin 3 DroidCon Berlin 2022

Slide 2

Slide 2 text

Hello, World! @BoD @MartinBonnin apollographql/apollo-kotlin

Slide 3

Slide 3 text

Hello, World! @MartinBonnin apollographql/apollo-kotlin

Slide 4

Slide 4 text

Agenda! The big things The smaller things The upcoming things

Slide 5

Slide 5 text

Apollo Kotlin

Slide 6

Slide 6 text

https://github.com/apollographql/apollo-kotlin

Slide 7

Slide 7 text

The big things

Slide 8

Slide 8 text

Kotlin Multiplatform

Slide 9

Slide 9 text

Kotlin Multiplatform dependencies { // JVM only, supports cache implementation("com.apollographql.apollo:apollo-runtime:2.5.12") // Coroutines extensions implementation("com.apollographql.apollo:apollo-coroutines-support:2.5.12") // Multiplatform, no cache implementation("com.apollographql.apollo:apollo-runtime-kotlin:2.5.12") } dependencies { // Multiplatform, supports cache implementation("com.apollographql.apollo3:apollo-runtime:3.3.2") }

Slide 10

Slide 10 text

Kotlin Multiplatform // Queries are suspend functions val response = apolloClient.query(query).execute() // Mutations are suspend functions val response = apolloClient.mutation(query).execute() // Subscriptions are Flows apolloClient.subscription(subscription).toFlow().collect { println(it.data) }

Slide 11

Slide 11 text

Kotlin Multiplatform dependencies { // Multiplatform, supports cache implementation("com.apollographql.apollo3:apollo-normalized-cache:3.3.2") } val cacheFactory = MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024) val apolloClient = ApolloClient.Builder() .serverUrl("https://...") .normalizedCache(cacheFactory) // Extension function .build()

Slide 12

Slide 12 text

Kotlin Multiplatform ● Networking ○ OkHttp ○ NSURLConnection ○ Ktor2 ● Persistence ○ SQLDelight ● Concurrency ○ Stately-inspired

Slide 13

Slide 13 text

Codegen - 2.x fragment humanDetails on Human { height } query GetHero { hero { name ... on Droid { primaryFunction } ...humanDetails } } // Droid hero.asDroid?.primaryFunction // Human hero.fragments?.humanDetails?.height GraphQL Kotlin

Slide 14

Slide 14 text

Json response { "data" { "hero": { "name": "C-3PO", "primaryFunction": "translation" } } } Json { "data" { "hero": { "name": "Leia", "height": "155" } } } Json

Slide 15

Slide 15 text

Codegen - 3.x fragment humanDetails on Human { height } query GetHero { hero { name ... on Droid { primaryFunction } ...humanDetails } } when (hero) { is DroidHero -> hero.primaryFunction is HumanHero -> hero.height } GraphQL Kotlin

Slide 16

Slide 16 text

Codegen - 3.x # Base interface interface HumanDetails(val height: Int) # Base interface sealed interface Hero(val name: String) data class DroidHero(override val name: String, val primaryFunction: String): Hero data class HumanHero(override val name: String, override val height: Int): Hero, HumanDetails data class OtherHero(override val name: String) Kotlin

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

Codegen fragment ancestors on Character { mum { name } dad { name } } query GetHeroAncestors { hero { ...ancestors } } GraphQL { "mum": { "name": "Padmé" }, "dad": { "name": "Anakin" } } Json

Slide 19

Slide 19 text

Codegen fragment ancestors1 on Character { mum { ...ancestors2 } dad { ...ancestors2 } } fragment ancestors2 on Character { mum { name } dad { name } } query GetHeroAncestors { hero { ...ancestors1 } } GraphQL { "hero": { "mum": { "mum": "ancestor #0", "dad": "ancestor #1" }, "data": { "mum": "ancestor #2", "dad": "ancestor #3" } } } Json

Slide 20

Slide 20 text

Codegen fragment ancestors1 on Character { mum { ...ancestors2 } dad { ...ancestors2 } } fragment ancestors2 on Character { mum { ...ancestors3 } dad { ...ancestors3 } } fragment ancestors3 on Character { mum { name } dad { name } } query GetHeroAncestors { hero { ...ancestors1 } } GraphQL { "hero": { "mum": { "mum": { "mum": "ancestor #0", "dad": "ancestor #1" }, "data": { "mum": "ancestor #2", "dad": "ancestor #3" } }, "data": { "mum": { "mum": "ancestor #4", "dad": "ancestor #5" }, "data": { "mum": "ancestor #6", "dad": "ancestor #7" } } } } Json

Slide 21

Slide 21 text

Codegen fragment ancestors1 on Character { mum { ...ancestors2 } dad { ...ancestors2 } } fragment ancestors2 on Character { mum { ...ancestors3 } dad { ...ancestors3 } } fragment ancestors3 on Character { mum { ...ancestors4 } dad { ...ancestors4 } } fragment ancestors4 on Character { mum { name } dad { name } } query GetHeroAncestors { hero { ...ancestors1 } } GraphQL "mum": { "mum": { "mum": "ancestor #0", "dad": "ancestor #1" }, "data": { "mum": "ancestor #2", "dad": "ancestor #3" } }, "data": { "mum": { "mum": "ancestor #4", "dad": "ancestor #5" }, "data": { "mum": "ancestor #6", "dad": "ancestor #7" } } }, "data": { "mum": { "mum": { "mum": "ancestor #8", "dad": "ancestor #9" }, "data": { "mum": "ancestor #10", "dad": "ancestor #11" } }, "data": { "mum": { "mum": "ancestor #12", "dad": "ancestor #13" }, "data": { Json

Slide 22

Slide 22 text

"data": { "mum": { "mum": "ancestor #28", "dad": "ancestor #29" }, "data": { "mum": "ancestor #30", "dad": "ancestor #31" } } } } }, "data": { "mum": { "mum": { "mum": { "mum": { "mum": "ancestor #32", "dad": "ancestor #33" }, "data": { "mum": "ancestor #34", "dad": "ancestor #35" } }, "data": { "mum": { "mum": "ancestor #36", "dad": "ancestor #37" }, "data": { "mum": "ancestor #38", "dad": "ancestor #39" } } }, "data": { "mum": { "mum": { "mum": "ancestor #40", "dad": "ancestor #41" }, "data": { "mum": "ancestor #42", "dad": "ancestor #43" } }, "data": { "mum": { "mum": "ancestor #44", "dad": "ancestor #45" }, "data": { "mum": "ancestor #46", "dad": "ancestor #47" } } } }, "data": { "mum": { "mum": { "mum": { "mum": "ancestor #48", "dad": "ancestor #49" }, "data": { "mum": "ancestor #50", "dad": "ancestor #51" } }, "data": { "mum": { "mum": "ancestor #52", "dad": "ancestor #53" }, "data": { "mum": "ancestor #54", "dad": "ancestor #55" } } }, { "hero": { "mum": { "mum": { "mum": { "mum": { "mum": { "mum": "ancestor #0", "dad": "ancestor #1" }, "data": { "mum": "ancestor #2", "dad": "ancestor #3" } }, "data": { "mum": { "mum": "ancestor #4", "dad": "ancestor #5" }, "data": { "mum": "ancestor #6", "dad": "ancestor #7" } } }, "data": { "mum": { "mum": { "mum": "ancestor #8", "dad": "ancestor #9" }, "data": { "mum": "ancestor #10", "dad": "ancestor #11" } }, "data": { "mum": { "mum": "ancestor #12", "dad": "ancestor #13" }, "data": { "mum": "ancestor #14", "dad": "ancestor #15" } } } }, "data": { "mum": { "mum": { "mum": { "mum": "ancestor #16", "dad": "ancestor #17" }, "data": { "mum": "ancestor #18", "dad": "ancestor #19" } }, "data": { "mum": { "mum": "ancestor #20", "dad": "ancestor #21" }, "data": { "mum": "ancestor #22", "dad": "ancestor #23" } } }, "data": { "mum": { "mum": { "mum": "ancestor #24", "dad": "ancestor #25" }, "data": { "mum": "ancestor #26", "dad": "ancestor #27" } }, Codegen fragment ancestors1 on Character { mum { ...ancestors2 } dad { ...ancestors2 } } fragment ancestors2 on Character { mum { ...ancestors3 } dad { ...ancestors3 } } fragment ancestors3 on Character { mum { ...ancestors4 } dad { ...ancestors4 } } fragment ancestors4 on Character { mum { ...ancestors5 } dad { ...ancestors5 } } fragment ancestors5 on Character { mum { ...ancestors6 } dad { ...ancestors6 } } fragment ancestors6 on Character { mum { name } dad { name } } query GetHeroAncestors { hero { ...ancestors1 } } GraphQL Json "data": { "mum": { "mum": { "mum": "ancestor #56", "dad": "ancestor #57" }, "data": { "mum": "ancestor #58", "dad": "ancestor #59" } }, "data": { "mum": { "mum": "ancestor #60", "dad": "ancestor #61" }, "data": { "mum": "ancestor #62", "dad": "ancestor #63" } } } } } } }

Slide 23

Slide 23 text

Codegen - operationBased fragment humanDetails on Human { height } query GetHero { hero { name ... on Droid { primaryFunction } ...humanDetails } } // Droid hero.onDroid?.primaryFunction // Human hero?.humanDetails?.height GraphQL Kotlin

Slide 24

Slide 24 text

class HumanDetails(val height: Int) class OnDroid(val primaryFunction: String) class Hero(val name: String, val onDroid: OnDroid?, val humanDetails: HumanDetails?) Codegen - operationBased Kotlin

Slide 25

Slide 25 text

Codegen - tradeoffs

Slide 26

Slide 26 text

Codegen - compat ● For compatibility with 2.x ● Duplicates some fields ● Has extra ‘fragments’ ● Will be removed

Slide 27

Slide 27 text

Performance - 2.x Network Json Map data class

Slide 28

Slide 28 text

Performance - Json Streaming Network Json data class Parsing is mostly free

Slide 29

Slide 29 text

Performance - Json Streaming Network Json data class* * with some exceptions in operationBased codegen Parsing is mostly free

Slide 30

Slide 30 text

Performance - SQLite batching query GetFriends { hero { id name friends { id name } } } GraphQL { "hero": { "id": "1001", "name": "Luke", "friends": [ { "id": "1002", "name": "Leia" }, { "id": "1003", "name": "Han Solo" }, { "id": "1004", "name": "Chewbacca" } ] } } Json

Slide 31

Slide 31 text

Performance - SQLite batching https://github.com/apollographql/apollo-kotlin/pull/2895 get(“1001”) get(“1002”) get(“1003”) get(“1004”) get(“1001”) get(“1002”, “1003”, “1004”) X3 faster

Slide 32

Slide 32 text

Performance - SQLite batching ● 2.x executes 4 SQL queries ● 3.x executes 2 SQL queries

Slide 33

Slide 33 text

The smaller things

Slide 34

Slide 34 text

Apollo AST (Abstract Syntax Tree)

Slide 35

Slide 35 text

https://spec.graphql.org/draft/#sec-Appendix-Grammar-Summary

Slide 36

Slide 36 text

Apollo AST val document = query.buffer() .parseAsGQLDocument() .valueAssertNoErrors() // "GetUser" val operationName = document.definitions.first() .cast() .name Kotlin query GetUser { viewer { name } } GraphQL

Slide 37

Slide 37 text

Apollo AST query GetUser { viewer { name # role is only supported with # servers version 3+ role @since(version: 3) } } val document = query.buffer() .parseAsGQLDocument() .valueAssertNoErrors() // Remove all fields that are not supported by the current server version val transformed = document.transform { if (it is GQLField && (it.minVersion() ?: 0) > currentVersion) { TransformResult.Delete } else { TransformResult.Continue } } GraphQL Kotlin

Slide 38

Slide 38 text

@nonnull ?!

Slide 39

Slide 39 text

@nonnull type User { # This field is nullable but # it should really be non-null email: String } query GetUser { user { # force a non-null Kotlin property # in this operation email @nonnull } } data class User( // non-null ! val email: String ) operation.graphql Kotlin schema.graphqls

Slide 40

Slide 40 text

@nonnull type User { # This field is nullable but # it should really be non-null email: String } # extra.graphqls # Make ‘email’ non-null in the schema extend type User @nonnul(fields: "email") data class User( // non-null ! val email: String ) extra.graphqls Kotlin schema.graphqls

Slide 41

Slide 41 text

Declarative cache type Query { book(isbn: String!): Book! author: Author! } type Author { id:ID! firstName: String! lastName: String! books: [Book!]! } type Book { isbn: String! title: String! author: Author } GraphQL schema.graphqls # Use "id" as cache key for Author extend type Author @typePolicy(keyFields: "id") # Use "isbn" as cache key for Book extend type Book @typePolicy(keyFields: "isbn") # Use the "id" argument to look in the cache # before doing a network request extend type Query @fieldPolicy(forField: "book", keyArgs: "isbn") extra.graphqls

Slide 42

Slide 42 text

One last thing val mockServer = MockServer() val apolloClient = ApolloClient.Builder() .serverUrl(mockServer.url()) .build() mockServer.enqueue(""" { "data": { "hero": { "name": "Luke" } } } """.trimIndent()) apolloClient.query(HeroNameQuery()).execute() Kotlin

Slide 43

Slide 43 text

Recap ● 100% Kotlin multiplatform ● New codegen ● Performance ● AST ● Client directives ● Declarative cache ● MockServer

Slide 44

Slide 44 text

The upcoming things

Slide 45

Slide 45 text

“Coroutines are really cool until you try to call them from Java” – every single Java developer

Slide 46

Slide 46 text

https://github.com/apollographql/apollo-kotlin/issues/3694

Slide 47

Slide 47 text

Upcoming ● Pagination APIs ● Test Builders ● New memory model ● Schema transforms ● Your feature!

Slide 48

Slide 48 text

Don’t be a stranger apollographql/apollo-kotlin #apollo-kotlin @martinbonnin

Slide 49

Slide 49 text

Thanks!

Slide 50

Slide 50 text

Credits ● “Cairn revisited” photo by Pascal Tribillon ○ https://flic.kr/p/nJ9oqa ● Star wars illustrations by Vecteezy ○ https://www.vecteezy.com/free-vector/star-wars