Slide 1

Slide 1 text

Caching your data graph Offline & Reactive apps with Apollo Kotlin

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

What is GraphQL? An open source language to describe and run your API

Slide 4

Slide 4 text

What is GraphQL? ● Schema ○ Int, Float, String, Boolean ○ Objects, Interfaces, Unions ○ Lists ○ Nullability ● Introspection ● Deprecation

Slide 5

Slide 5 text

APIs in a REST world

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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" } } }

Slide 12

Slide 12 text

Caching entities

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

How to deal with partial entities

Slide 15

Slide 15 text

How to deal with partial entities null means not cached? null means null?

Slide 16

Slide 16 text

How to deal with partial entities 👎 both are User - should share cache

Slide 17

Slide 17 text

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 👎

Slide 18

Slide 18 text

Entering Cache normalization Response → List A Record is a Map

Slide 19

Slide 19 text

Cache normalization { "data": { "user": { "id": "42", "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" } } } Response

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

What if there’s no id? { "data": { "user": { "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" } } }

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

What if there are several paths? { "data": { "user": {…} "comments": [ { "text": "#dcldn22 is awesome 😎󰏅", "date": "2022-10-28T10:00Z", "user": { "login": "BoD", "avatarUrl": "https://" }, }, ], } }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Always query your ids

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Apollo Kotlin

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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()

Slide 34

Slide 34 text

Watchers

Slide 35

Slide 35 text

The cache updates after a mutation mutation { updateUser(id: "42", status: "At dcldn22 😃") { id status } }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Single source of truth

Slide 38

Slide 38 text

Conclusion ● Type-safe language + Tooling = 💜 ● Offline support is one line ✈ ● Don’t forget your ids!

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

For inspiration 🎊 github.com/joreilly/Confetti

Slide 41

Slide 41 text

Merci! @BoD @MartinBonnin apollographql/apollo-kotlin

Slide 42

Slide 42 text

It depends.

Slide 43

Slide 43 text

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")

Slide 44

Slide 44 text

Optimistic updates

Slide 45

Slide 45 text

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")

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Caching entities