$30 off During Our Annual Pro Sale. View Details »

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. What’s new in Apollo Kotlin 3 DroidCon Berlin 2022

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

  3. Hello, World! @MartinBonnin apollographql/apollo-kotlin

  4. Agenda! The big things The smaller things The upcoming things

  5. Apollo Kotlin

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

  7. The big things

  8. Kotlin Multiplatform

  9. 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") }
  10. 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) }
  11. 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()
  12. Kotlin Multiplatform • Networking ◦ OkHttp ◦ NSURLConnection ◦ Ktor2

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

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

    dad { name } } query GetHeroAncestors { hero { ...ancestors } } GraphQL { "mum": { "name": "Padmé" }, "dad": { "name": "Anakin" } } Json
  19. 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
  20. 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
  21. 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
  22. "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" } } } } } } }
  23. 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
  24. class HumanDetails(val height: Int) class OnDroid(val primaryFunction: String) class Hero(val

    name: String, val onDroid: OnDroid?, val humanDetails: HumanDetails?) Codegen - operationBased Kotlin
  25. Codegen - tradeoffs

  26. Codegen - compat • For compatibility with 2.x • Duplicates

    some fields • Has extra ‘fragments’ • Will be removed
  27. Performance - 2.x Network Json Map<String, Any?> data class

  28. Performance - Json Streaming Network Json data class Parsing is

    mostly free
  29. Performance - Json Streaming Network Json data class* * with

    some exceptions in operationBased codegen Parsing is mostly free
  30. 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
  31. 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
  32. Performance - SQLite batching • 2.x executes 4 SQL queries

    • 3.x executes 2 SQL queries
  33. The smaller things

  34. Apollo AST (Abstract Syntax Tree)

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

  36. Apollo AST val document = query.buffer() .parseAsGQLDocument() .valueAssertNoErrors() // "GetUser"

    val operationName = document.definitions.first() .cast<GQLOperationDefinition>() .name Kotlin query GetUser { viewer { name } } GraphQL
  37. 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
  38. @nonnull ?!

  39. @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
  40. @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
  41. 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
  42. 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
  43. Recap • 100% Kotlin multiplatform • New codegen • Performance

    • AST • Client directives • Declarative cache • MockServer
  44. The upcoming things

  45. “Coroutines are really cool until you try to call them

    from Java” – every single Java developer
  46. https://github.com/apollographql/apollo-kotlin/issues/3694

  47. Upcoming • Pagination APIs • Test Builders • New memory

    model • Schema transforms • Your feature!
  48. Don’t be a stranger apollographql/apollo-kotlin #apollo-kotlin @martinbonnin

  49. Thanks!

  50. 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