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 ○ Lists ○ Nullability ● Introspection ● Deprecation (and experimental soon 🧪)

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

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Entering Cache normalization Response -> List A Record is a Map

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Cache normalization - Records { "data": { "user": CacheReference("42"), }, "42": { "id": "42", "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" } }

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

The field path is used as id { "data": { "user": CacheReference("data.user"), }, "data.user": { "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" } }

Slide 22

Slide 22 text

What if there are several paths? { "data": { "todo": [ { "title": "Write retrowave slides!", "checked": true, "user": { "login": "BoD", "avatarUrl": "https://" }, }, ], } }

Slide 23

Slide 23 text

The field path is used as id { "data": { "user": CacheReference("data.user"), }, "data.user": { "email": "[email protected]", "login": "BoD", "name": "Benoit Lubek" }, }

Slide 24

Slide 24 text

The field path is used as id { "data": { "user": CacheReference("data.user"), "todo": CacheReference("data.todo"), }, "data.user": { "email": "[email protected]", "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///" } }

Slide 25

Slide 25 text

The field path is used as id { "data": { "user": CacheReference("data.user"), "todo": CacheReference("data.todo"), }, "data.user": { "email": "[email protected]", "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

Slide 26

Slide 26 text

Always define your ids

Slide 27

Slide 27 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 ) )

Slide 28

Slide 28 text

Apollo Kotlin

Slide 29

Slide 29 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 30

Slide 30 text

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

Slide 31

Slide 31 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 32

Slide 32 text

Watchers

Slide 33

Slide 33 text

The cache updates after a mutation mutation { updateUser({ id: "42", status: "Au DevFest Lille 😃" }) { id status } }

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Single source of truth

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Where to go from there ● Apollo-Kotlin ○ #3566 (data age) ○ #3807 (pagination) ● Server side caching ○ @cacheControl ○ Automated Persisted Queries

Slide 38

Slide 38 text

For inspiration 🎊 github.com/joreilly/Confetti/pull/44

Slide 39

Slide 39 text

Questions?

Slide 40

Slide 40 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 41

Slide 41 text

It depends.

Slide 42

Slide 42 text

Optimistic updates

Slide 43

Slide 43 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 44

Slide 44 text

Life is hard!