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

Offline and Reactive apps with Apollo Kotlin

Avatar for mbonnin mbonnin
November 07, 2022

Offline and Reactive apps with Apollo Kotlin

Slides from Benoit Lubek and Martin Bonnin's talk at droidcon London 2022 🎃

Avatar for mbonnin

mbonnin

November 07, 2022
Tweet

More Decks by mbonnin

Other Decks in Technology

Transcript

  1. What is GraphQL? • Schema ◦ Int, Float, String, Boolean

    ◦ Objects, Interfaces, Unions ◦ Lists ◦ Nullability • Introspection • Deprecation
  2. How does it look in practice query UserQuery { user

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

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

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

    { id email login name } } { "data": { "user": { "id": "42", "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" } } }
  6. How to deal with partial entities query ViewerQuery { #

    Returns a User viewer { id email avatarUrl } } query UserQuery($id: String) { # Also a User user(id: $id) { id email login name } }
  7. Cache the HTTP response? { "data": { "viewer": { "id":

    "42", "email": "[email protected]", "avatarUrl": "http://…" } } } { "data": { "user": { "id": "42", "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" } } } key: ViewerQuery key: UserQuery(42) same entity, but different keys 👎
  8. Cache normalization { "data": { "user": { "id": "42", "email":

    "[email protected]", "login": "BoD", "name": "Benoit Lubek" } } } Response
  9. Cache normalization { "data": { "user": CacheReference("42"), }, "42": {

    "id": "42", "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" } } { "data": { "user": { "id": "42", "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" } } } Response 2 Records
  10. Adding fields { "data": { "user": CacheReference("42"), }, "42": {

    "id": "42", "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek", } }
  11. Adding fields { "data": { "user": CacheReference("42"), }, "42": {

    "id": "42", "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek", // New Record field "avatarUrl": "https://…", } }
  12. Adding fields { "data": { "user": CacheReference("42"), }, "42": {

    "id": "42", "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek", // New Record field "avatarUrl": "https://…", } } Ids!
  13. What if there’s no id? { "data": { "user": {

    "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" } } }
  14. Use the field’s path as key { "data": { "user":

    CacheReference("data.user"), }, "data.user": { "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" } }
  15. What if there are several paths? { "data": { "user":

    {…} "comments": [ { "text": "#dcldn22 is awesome 😎󰏅", "date": "2022-10-28T10:00Z", "user": { "login": "BoD", "avatarUrl": "https://" }, }, ], } }
  16. Use the field’s path as key { "data": { "user":

    CacheReference("data.user"), "comments": [CacheReference("data.comments[0]")], }, "data.user": { "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" }, "data.comments[0]": { "title": "Write retrowave slides!", "checked": true, "user": CacheReference("data.comments[0].user") }, "data.comments[0].user": { "login": "BoD", "avatarUrl": "https://" } } Duplication
  17. This is all typesafe { "data": { "user": CacheReference("42"), },

    "42": { "id": "42", "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" } } Data( user=User( id=42, [email protected], login=BoD, name=Benoit Lubek ) ) 2 Records 1 Data class
  18. Storage: in-memory or persistent val memoryCache = MemoryCacheFactory(maxSizeBytes = 5_000_000)

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

    apolloClient: ApolloClient = ApolloClient.Builder() .serverUrl(SERVER_URL) .normalizedCache(sqlCache) .build()
  20. 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()
  21. The cache updates after a mutation mutation { updateUser(id: "42",

    status: "At dcldn22 😃") { id status } }
  22. The cache updates after a mutation watch() mutate("At dcldn22 😃")

    // receives from network "At dcldn22 😃" // updates the cache "At dcldn22 😃" Coroutine 1 Coroutine 2 // receives from network "Work from home 🏡" // wait for cache updates
  23. Conclusion • Type-safe language + Tooling = 💜 • Offline

    support is one line ✈ • Don’t forget your ids!
  24. Where to go from here • Declarative cache ◦ Available

    in 3.0 • Client improvements ◦ apollo-normalized-cache-incubating ◦ #3566 (data age) ◦ #3807 (pagination) • Server side caching ◦ @cacheControl ◦ Automated Persisted Queries
  25. 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")
  26. 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")
  27. 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!] }