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

Offline and Reactive apps with Apollo Kotlin

mbonnin
November 07, 2022

Offline and Reactive apps with Apollo Kotlin

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

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!] }