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

Offline and Reactive apps with Apollo Kotlin

Offline and Reactive apps with Apollo Kotlin

mbonnin

June 11, 2022
Tweet

More Decks by mbonnin

Other Decks in Programming

Transcript

  1. Caching your data graph Offline & Reactive apps with Apollo

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

  3. What is GraphQL? An open source language to describe and

    run your API
  4. What is GraphQL? • Schema ◦ Int, Float, String, Boolean

    ◦ Objects, Interfaces ◦ Lists ◦ Nullability • Introspection • Deprecation (and experimental soon 🧪)
  5. APIs in a REST world

  6. https://apis.guru/graphql-voyager/

  7. How does it look in practice query UserQuery { user

    { id login } }
  8. How does it look in practice query UserQuery { user

    { id login avatar { small medium } } }
  9. How does it look in practice query UserQuery { user

    { id login name } } { "data": { "user": { "id": "42", "login": "BoD", "name": "Benoit Lubek" } } }
  10. How does it look in practice query UserQuery { user

    { id login name } } { "data": { "user": { "id": "42", "login": "BoD", "name": "Benoit Lubek" } } }
  11. How does it look in practice query UserQuery { user

    { id email login name } } { "data": { "user": { "id": "42", "email": "BoD@JRAF.org", "login": "BoD", "name": "Benoit Lubek" } } }
  12. Caching entities

  13. That doesn’t work with partial entities query ViewerQuery { viewer

    { # Returns a User id email avatarUrl } } query UserQuery($id: String) { user(id: $id) { # Also a User id email login name } }
  14. We could cache the HTTP response { "data": { "viewer":

    { "id": "42", "email": "BoD@JRAF.org", "avatarUrl": "http://…" } } } { "data": { "user": { "id": "42", "email": "BoD@JRAF.org", "login": "BoD", "name": "Benoit Lubek" } } }
  15. Entering Cache normalization Response -> List<Record> A Record is a

    Map<String, Any?>
  16. Cache normalization - Response { "data": { "user": { "id":

    "42", "email": "BoD@JRAF.org", "login": "BoD", "name": "Benoit Lubek" } } }
  17. Cache normalization - Records { "data": { "user": CacheReference("42"), },

    "42": { "id": "42", "email": "BoD@JRAF.org", "login": "BoD", "name": "Benoit Lubek" } }
  18. Adding fields { "data": { "user": CacheReference("42"), }, "42": {

    "id": "42", "email": "BoD@JRAF.org", "login": "BoD", "name": "Benoit Lubek", // New Record field "avatarUrl": "http://…" } }
  19. { "data": { "user": CacheReference("42"), }, "42": { "id": "42",

    "email": "BoD@JRAF.org", "login": "BoD", "name": "Benoit Lubek", // New Record field "avatarUrl": "http://…" } } Cache ids Ids!
  20. What if there’s no id? { "data": { "user": {

    "email": "BoD@JRAF.org", "login": "BoD", "name": "Benoit Lubek" } } }
  21. The field path is used as id { "data": {

    "user": CacheReference("data.user"), }, "data.user": { "email": "BoD@JRAF.org", "login": "BoD", "name": "Benoit Lubek" } }
  22. What if there are several paths? { "data": { "todo":

    [ { "title": "Write retrowave slides!", "checked": true, "user": { "login": "BoD", "avatarUrl": "https://" }, }, ], } }
  23. The field path is used as id { "data": {

    "user": CacheReference("data.user"), }, "data.user": { "email": "BoD@JRAF.org", "login": "BoD", "name": "Benoit Lubek" }, }
  24. The field path is used as id { "data": {

    "user": CacheReference("data.user"), "todo": CacheReference("data.todo"), }, "data.user": { "email": "BoD@JRAF.org", "login": "BoD", "name": "Benoit Lubek" }, "data.todo": { "title": "Write retrowave slides!", "checked": true, "user": CacheReference("data.todo.user") }, "data.todo[0].user": { "login": "Bod", "avatarUrl": "https///" } }
  25. The field path is used as id { "data": {

    "user": CacheReference("data.user"), "todo": CacheReference("data.todo"), }, "data.user": { "email": "BoD@JRAF.org", "login": "BoD", "name": "Benoit Lubek" }, "data.todo": { "title": "Write retrowave slides!", "checked": true, "user": CacheReference("data.todo.user") }, "data.todo.user": { "login": "Bod", "avatarUrl": "https///" } } Duplication
  26. Always define your ids

  27. This is all typesafe { "data": { "user": CacheReference("42"), },

    "42": { "id": "42", "email": "BoD@JRAF.org", "login": "BoD", "name": "Benoit Lubek" } } Data( user=User( id=42, email=BoD@JRAF.org, login=BoD, name=Benoit Lubek ) )
  28. Apollo Kotlin

  29. Storage: in-memory or persistent val memoryCache = MemoryCacheFactory(maxSizeBytes = 5_000_000)

    val apolloClient: ApolloClient = ApolloClient.Builder() .serverUrl(SERVER_URL) .normalizedCache(memoryCache) .build()
  30. Storage: in-memory or persistent val sqlCache = SqlNormalizedCacheFactory(context, "app.db") val

    apolloClient: ApolloClient = ApolloClient.Builder() .serverUrl(SERVER_URL) .normalizedCache(sqlCache) .build()
  31. Storage: in-memory and persistent val memoryCache = MemoryCacheFactory(maxSizeBytes = 5_000_000)

    val sqlCache = SqlNormalizedCacheFactory(context, "app.db") val memoryThenSqlCache = memoryCache.chain(sqlCache) val apolloClient: ApolloClient = ApolloClient.Builder() .serverUrl(SERVER_URL) .normalizedCache(memoryThenSqlCache) .build()
  32. Watchers

  33. The cache updates after a mutation mutation { updateUser({ id:

    "42", status: "Au DevFest Lille 😃" }) { id status } }
  34. The cache updates after a mutation watch() // receives from

    network "En télétravail 🏡" // wait for cache updates mutate("Au DevFest Lille 😃") // receives from network "Au DevFest Lille 😃" // updates the cache "Au DevFest Lille 😃" Coroutine 1 Coroutine 2
  35. Single source of truth

  36. Conclusion • Type-safe language + Tooling = 💜 • Offline

    support is one line 😎 • Don’t forget your ids!
  37. Where to go from there • Apollo-Kotlin ◦ #3566 (data

    age) ◦ #3807 (pagination) • Server side caching ◦ @cacheControl ◦ Automated Persisted Queries
  38. For inspiration 🎊 github.com/joreilly/Confetti/pull/44

  39. Questions?

  40. Declarative cache type User { id: ID! name: String! }

    type Query { user(id: ID!): User } extend type User @typePolicy(keyFields: "id") extend type Query @fieldPolicy(forField: "user", keyArgs: "id")
  41. It depends.

  42. Optimistic updates

  43. Schema # schema.graphqls type Speaker implements Node { id: ID!

    name: String! company: String session(name: String!): Session sessions(first: Int, after: ID, orderBy: SessionOrder): [Session!] }
  44. Life is hard!