Upgrade to Pro — share decks privately, control downloads, hide ads and more …

What's new in Apollo Kotlin 3

What's new in Apollo Kotlin 3

Apollo Kotlin is a type-safe, caching GraphQL client - you may also know it as Apollo Android.
Why the name change? Well, version 3 is a major release and is now written 100% in Kotlin! It brings, among other things:

* a coroutines and Flow based API
* Kotlin Multiplatform support
* new codegen options
* test builders
* and more!

In this session we’ll look at what Apollo Kotlin is, how to use it in a project, and dive into the latest and upcoming features.

mbonnin

July 06, 2022
Tweet

More Decks by mbonnin

Other Decks in Programming

Transcript

  1. 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") }
  2. 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) }
  3. 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()
  4. Kotlin Multiplatform • Networking ◦ OkHttp ◦ NSURLConnection ◦ Ktor2

    • Persistence ◦ SQLDelight • Concurrency ◦ Stately-inspired
  5. 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
  6. Json response { "data" { "hero": { "name": "C-3PO", "primaryFunction":

    "translation" } } } Json { "data" { "hero": { "name": "Leia", "height": "155" } } } Json
  7. 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
  8. 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
  9. Codegen fragment ancestors on Character { mum { name }

    dad { name } } query GetHeroAncestors { hero { ...ancestors } } GraphQL { "mum": { "name": "Padmé" }, "dad": { "name": "Anakin" } } Json
  10. 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
  11. 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
  12. 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
  13. "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" } } } } } } }
  14. 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
  15. class HumanDetails(val height: Int) class OnDroid(val primaryFunction: String) class Hero(val

    name: String, val onDroid: OnDroid?, val humanDetails: HumanDetails?) Codegen - operationBased Kotlin
  16. Codegen - compat • For compatibility with 2.x • Duplicates

    some fields • Has extra ‘fragments’ • Will be removed
  17. Performance - Json Streaming Network Json data class* * with

    some exceptions in operationBased codegen Parsing is mostly free
  18. 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
  19. Apollo AST val document = query.buffer() .parseAsGQLDocument() .valueAssertNoErrors() // "GetUser"

    val operationName = document.definitions.first() .cast<GQLOperationDefinition>() .name Kotlin query GetUser { viewer { name } } GraphQL
  20. 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
  21. @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
  22. @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
  23. 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
  24. 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
  25. Recap • 100% Kotlin multiplatform • New codegen • Performance

    • AST • Client directives • Declarative cache • MockServer
  26. “Coroutines are really cool until you try to call them

    from Java” – every single Java developer
  27. Upcoming • Pagination APIs • Test Builders • New memory

    model • Schema transforms • Your feature!
  28. 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