Slide 1

Slide 1 text

GraphQL for mobile clients Ivan Savytskyi 1 https://www.shopify.ca/careers https://www.shopify.ca

Slide 2

Slide 2 text

Introduction 01 Agenda 2 Wait… REST? 02 GraphQL Silver bullet? 03 GraphQL on clients 04 Apollo GraphQL client 05

Slide 3

Slide 3 text

3 Introduction 01

Slide 4

Slide 4 text

● A query language for your API ● Programming language ● Developed by Facebook (2012, open sourced 2015) ● GraphQL Foundation part of Linux Foundation (2018) GraphQL 4

Slide 5

Slide 5 text

● Document ● Executable definitions ○ Operation definitions: query, mutation, subscription ○ Fragment definitions ● Type system (IDL / SDL) ○ Type system definitions ○ Type system extensions Language 5 https://spec.graphql.org/

Slide 6

Slide 6 text

Queries 6 query ShopInfo { shop { name description } } https://graphql.org/learn/queries/

Slide 7

Slide 7 text

Queries 7 query ShopInfo { shop { name description } } https://graphql.org/learn/queries/

Slide 8

Slide 8 text

Queries 8 query ShopInfo { shop { name description } } https://graphql.org/learn/queries/

Slide 9

Slide 9 text

Queries 9 query ShopInfo { shop { name description } } { "data": { "shop": { "name": "graphql", "description": "An example shop." } } } https://graphql.org/learn/queries/

Slide 10

Slide 10 text

Queries 10 query Products($after: String!, $pageSize: Int!) { products(after:$after, first: $pageSize) { edges { product: node { title } } } } https://graphql.org/learn/queries/

Slide 11

Slide 11 text

Queries 11 query Products($after: String!, $pageSize: Int!) { products(after:$after, first: $pageSize) { edges { product: node { title } } } } https://graphql.org/learn/queries/ variables arguments

Slide 12

Slide 12 text

Queries 12 query Products($after: String!, $pageSize: Int!) { products(after:$after, first: $pageSize) { edges { product: node { title } } } } https://graphql.org/learn/queries/ alias

Slide 13

Slide 13 text

Queries 13 query Products( $after: String!, $pageSize: Int! ) { products( after:$after, first: $pageSize ) { edges { product: node { title } } } } { "data": { "products": { "edges": [ { "product": { "title": "Snare Boot" } }, { "product": { "title": "Neptune Boot" } } ] } } } https://graphql.org/learn/queries/

Slide 14

Slide 14 text

Queries 14 query Products($size: Int) { products(first: $size) { edges { product: node { ...ProductPreview } } } } fragment ProductPreview on Product { id title } https://graphql.org/learn/queries/#fragments

Slide 15

Slide 15 text

Queries 15 query Products($size: Int) { products(first: $size) { edges { product: node { ...ProductPreview } } } } fragment ProductPreview on Product { id title } https://graphql.org/learn/queries/#fragments

Slide 16

Slide 16 text

Queries 16 query Products($size: Int) { products(first: $size) { edges { product: node { ...ProductPreview } } } } fragment ProductPreview on Product { id title } { "data": { "products": { "edges": [ { "product": { "id": "gid://shopify/Product/3665442689", "title": "Snare Boot" } }, { "product": { "id": "gid://shopify/Product/5628638401", "title": "Neptune Boot" } } ] } } } https://graphql.org/learn/queries/#fragments

Slide 17

Slide 17 text

Queries 17 query ProductDetails { node(id: "gid://shopify/Product/3665442689") { ... on Product { id title } } } https://graphql.org/learn/queries/#inline-fragments

Slide 18

Slide 18 text

Queries 18 query ProductDetails { node(id: "gid://shopify/Product/3665442689") { ... on Product { id title } } } https://graphql.org/learn/queries/#inline-fragments

Slide 19

Slide 19 text

Queries 19 query ProductDetails { node(id: "gid://shopify/Product/3665442689") { ... on Product { id title } } } { "data": { "node": { "id": "gid://shopify/Product/3665442689", "title": "Product title" } } } https://graphql.org/learn/queries/#inline-fragments

Slide 20

Slide 20 text

Mutations 20 mutation UpdateCustomer( $accessToken: String!, $customerUpdate: CustomerUpdateInput! ) { customerUpdate( customerAccessToken: $accessToken, customer: $customerUpdate ) { customer { firstName lastName } } } https://graphql.org/learn/queries/#mutations

Slide 21

Slide 21 text

Mutations 21 mutation UpdateCustomer( $accessToken: String!, $customerUpdate: CustomerUpdateInput! ) { customerUpdate( customerAccessToken: $accessToken, customer: $customerUpdate ) { customer { firstName lastName } } } https://graphql.org/learn/queries/#mutations

Slide 22

Slide 22 text

Mutations 22 mutation UpdateCustomer( $accessToken: String!, $customerUpdate: CustomerUpdateInput! ) { customerUpdate( customerAccessToken: $accessToken, customer: $customerUpdate ) { customer { firstName lastName } } } https://graphql.org/learn/schema/#input-types input type

Slide 23

Slide 23 text

Mutations 23 mutation UpdateCustomer( $accessToken: String!, $customerUpdate: CustomerUpdateInput! ) { customerUpdate( customerAccessToken: $accessToken, customer: $customerUpdate ) { customer { firstName lastName } } } https://graphql.org/learn/queries/#mutations

Slide 24

Slide 24 text

Mutations 24 mutation UpdateCustomer( $accessToken: String!, $customerUpdate: CustomerUpdateInput! ) { customerUpdate( customerAccessToken: $accessToken, customer: $customerUpdate ) { customer { firstName lastName } } } https://graphql.org/learn/queries/#mutations

Slide 25

Slide 25 text

Mutations 25 mutation UpdateCustomer( $accessToken: String!, $customerUpdate: CustomerUpdateInput! ) { customerUpdate( customerAccessToken: $accessToken, customer: $customerUpdate ) { customer { firstName lastName } } } Variables: { "accessToken": "customer_access_token", "customerUpdate": { "firstName": "Ivan", "lastName": "Savytskyi" } } https://graphql.org/learn/queries/#mutations

Slide 26

Slide 26 text

Subscriptions 26 subscription RepoComments($repo: String!) { commentAdded(repoFullName: $repo) { id content } }

Slide 27

Slide 27 text

Subscriptions 27 subscription RepoComments($repo: String!) { commentAdded(repoFullName: $repo) { id content } }

Slide 28

Slide 28 text

Subscriptions 28 subscription RepoComments($repo: String!) { commentAdded(repoFullName: $repo) { id content } }

Slide 29

Slide 29 text

Demo GraphiQL 29 https://github.com/graphql/graphiql

Slide 30

Slide 30 text

30 Wait... REST? 02

Slide 31

Slide 31 text

GET /admin/api/2021-04/collections/{collection_id}.json HTTP/1.1 200 OK { "collection": { "id": 841564295, "products_count": 1, "title": "IPods", "body_html": "

The best selling ipod ever

", "image": { "width": 123, "height": 456, "src": "https://cdn.shopify.com/s/files/1/collections/ipod_nano_8gb.jpg" } } } REST 31

Slide 32

Slide 32 text

● Server always in charge ● Response data structure fixed ● Over/under fetching ● N+1 problem * ● Schemaless * ( OpenAPI) REST 32

Slide 33

Slide 33 text

● CORBA ● SOAP ● RPC / XML RPC / JSON RPC / gRPC ● etc. Others 33

Slide 34

Slide 34 text

GET /model.json?paths=["user.name", "user.surname", "user.address"] HTTP/1.1 200 OK { user: { name: "Frank", surname: "Underwood", address: "1600 Pennsylvania Avenue, Washington, DC" } } Falcor from Netflix 34 https://netflix.github.io/falcor/

Slide 35

Slide 35 text

35 Silver bullet? 03

Slide 36

Slide 36 text

● Tries to solve fixed structure problem ● Makes client to be “in charge” ● Scaling pretty well ● N+1 problem * ● Type safety * ● Documentation ● Fields usage tracking ● GraphQL is a language specification ● Schema based codegen GraphQL Pros 36

Slide 37

Slide 37 text

● Learning curve (especially on server side) ● Extra layer ● GraphQL as SQL for your DB ( Hasura) ● Http caching ● Query complexity ● N + 1 problem GraphQL Cons 37

Slide 38

Slide 38 text

query { heroes { name friends { name } } } GraphQL N + 1 problem 38 { "heroes": [ { "name": "R2-D2", "friends": [ { "name": "Luke Skywalker" }, { "name": "Han Solo" }, { "name": "Leia Organa" } ] }, { "name": "Han Solo", "friends": [ { "name": "Luke Skywalker" }, { "name": "C-3PO" }, { "name": "Leia Organa" } ] } ] }

Slide 39

Slide 39 text

● Learning curve (especially on server side) ● Extra layer ● GraphQL as SQL for your DB ( Hasura) ● Http caching ● Query complexity ● N + 1 problem ● Cardinality of query ● Client side normalized caching GraphQL Cons 39

Slide 40

Slide 40 text

40 GraphQL on client 04

Slide 41

Slide 41 text

GraphQL Http spec 41 Request: GET http://myapi/graphql?query={hero{name}} POST http://myapi/graphql { "query": "query MyHero {hero{name}}", "operationName": "MyHero", "variables": { "myVariable": "someValue", ... } } Response: { "data": { ... }, "errors": [ ... ], "extensions": { ... }, }

Slide 42

Slide 42 text

GraphQL On client 42 ● REST way * ● Unsafe GraphQL operation builder ● Schema first codegen ● Query first codegen

Slide 43

Slide 43 text

GraphQL REST way 43 fun buildCollectionsQueryString(): String { return """ { "query": "query { collections(first:${Params.PAGE_SIZE}) { edges { node { id title } } } }" } """.trimIndent() }

Slide 44

Slide 44 text

GraphQL REST way 44 internal interface RetrofitService { @POST(GRAPHQL_URL_PATH) @Headers(GraphQlUtil.GRAPHQL_CONTENT_TYPE, GraphQlUtil.GRAPHQL_ACCEPT) suspend fun getCollection( @Body graphQLQuery: RequestBody ): Response } retrofitService.getCollection( buildCollectionsQueryString() .toRequestBody("text/plain".toMediaTypeOrNull()) )

Slide 45

Slide 45 text

GraphQL REST way 45 Pros: ● Straightforward ● No extra dependencies Cons: ● Manual ● Unsafe ● Non scalable

Slide 46

Slide 46 text

GraphQL Unsafe builder 46 GraphQL code: query { notes { id createdDate content author { name avatarUrl(size: 100) } } } Kotlin code: Kraph* { query { fieldObject("notes") { field("id") field("createdDate") field("content") fieldObject("author") { field("name") field( "avatarUrl", mapOf("size" to 100) ) } } } } https://github.com/VerachadW/kraph

Slide 47

Slide 47 text

GraphQL Unsafe builder 47 Pros: ● Straightforward ● Kotlin DSL builder for GraphQL request payload Cons: ● Manual ● Unsafe ● Non scalable

Slide 48

Slide 48 text

GraphQL On client 48 ● Document ● Executable definitions ○ Operation definitions: query, mutation, subscription ○ Fragment definitions ● Type system (IDL / SDL) ○ Type system definitions ○ Type system extensions

Slide 49

Slide 49 text

GraphQL Schema 49 type Shop { name: String! description: String } type Product { id: ID! title: String! } type ProductConnection { edges: [ProductEdge!]! } type ProductEdge { node: Product! } type Query { shop: Shop products(first: Int!): ProductConnection! }

Slide 50

Slide 50 text

GraphQL Schema first codegen 50 GraphQL query: query { shop { name description } products(first = 10) { edges { node { id title } } } Kotlin DSL builder: val query = Query { shop { name description } products(first = 10) { edges { node { id title } } }

Slide 51

Slide 51 text

GraphQL Schema first codegen 51 val query = Query { shop { name description } } val result = graphClient.queryGraph(query).await() with(result as GraphCallResult.Success) { assertThat(response.hasErrors).isFalse() assertThat(response.data).isNotNull() assertThat(response.data!!.shop.name).isEqualTo("graphql") assertThat(response.data!!.shop.description) .isEqualTo("An example shop with GraphQL.") }

Slide 52

Slide 52 text

GraphQL Schema first codegen 52 Pros: ● Typesafe GraphQL request payload builder ● Typesafe GraphQL response models codegen * Cons: ● Custom DSL ● Unsafe when accessing missing field ● Keep schema in sync ● Non scalable

Slide 53

Slide 53 text

GraphQL Query first codegen 53 GraphQL query: query HeroDetails { heroes { name appearsIn friends { name } } } class HeroDetails : Query { override fun queryDocument(): String = QUERY_DOCUMENT data class Data( val heroes: List? ) : Query.Data { data class Heroes( val name: String, val appearsIn: List, val friends: List ) { data class Friends( val name: String ) } } } GraphQL schema: type Query { heroes: [Character!] } interface Character { name: String! appearsIn: [Episode!]! friends: [Character!]! } enum Episode { NEWHOPE EMPIRE JEDI }

Slide 54

Slide 54 text

GraphQL Query first codegen 54 object HeroDetails_ResponseAdapter : ResponseAdapter { val RESPONSE_NAMES: List = listOf("heroes") ... object Heroes : ResponseAdapter { val RESPONSE_NAMES: List = listOf("name", "appearsIn", "friends") override fun fromResponse(reader: JsonReader): HeroDetails.Data.Hero { var name: String? = null var appearsIn: Episode? = null var friends: HeroDetails.Data.Heroes.Friends? = null reader.beginObject() while(true) { when (reader.selectName(RESPONSE_NAMES)) { 0 -> name = StringResponseAdapter.fromResponse(reader) 1 -> appearsIn = Episode_ResponseAdapter.list().fromResponse(reader) 2 -> friends = Friends.list().fromResponse(reader) else -> break } } ...

Slide 55

Slide 55 text

GraphQL Query first codegen 55 Pros: ● Typesafe ● GraphQL query as source of truth ● Scalable ● Shareable Cons: ● Keep schema in sync ● Any changes leads to out of date ● Modularization ● Normalized cache

Slide 56

Slide 56 text

56 Apollo GraphQL client 05

Slide 57

Slide 57 text

Apollo 57 Apollo is a GraphQL client that generates Java and Kotlin models from GraphQL queries. These models give you a type-safe API to work with GraphQL servers. Apollo helps you keep your GraphQL query statements together, organized, and easy to access.

Slide 58

Slide 58 text

Apollo 58 ● Meteor Development Group -> Apollo GraphQL ● Apollo Android, iOS, JS clients ● Apollo Android open source in 2016 ● First release Java first ● Kotlin support 2018 (1.0.0v) ● Kotlin as first class language 2020

Slide 59

Slide 59 text

Apollo machinery 59 Compiler HeroQuery.graphql query Hero { hero { id name } } schema.json "data": { "__schema": { ... } } HeroQuery.kt / HeroQuery_Adapter.kt class HeroQuery : Query<...> { ... } Compiler Configuration

Slide 60

Slide 60 text

Apollo machinery 60 60 Gradle Plugin Compiler module Api module Compiler Configuration Apollo-core HttpCache NormalizedCache CoroutineSupport AndroidSupport RxSupport Runtime Runtime module

Slide 61

Slide 61 text

Apollo How-to 61 61 buildscript { classpath("com.apollographql.apollo:apollo-gradle-plugin:x.y.z") } apply plugin: 'com.apollographql.apollo' apollo { ... } dependencies { implementation("com.apollographql.apollo:apollo-api:x.y.z") implementation("com.apollographql.apollo:apollo-runtime:x.y.z") implementation("com.apollographql.apollo:apollo-http-cache:x.y.z") implementation("com.apollographql.apollo:apollo-normalized-cache-sqlite:x.y.z") implementation("com.apollographql.apollo:apollo-coroutines-support:x.y.z") } https://www.apollographql.com/docs/android/

Slide 62

Slide 62 text

Apollo How-to 62 62 /src |____main | |____graphql | | |____com | | | |____example | | | | |____api | | | | | |____schema.json | | | | | |____CollectionQuery.graphql | | | | | |____ProductsQuery.graphql schema.json - GraphQL schema from introspection query *.graphql - GraphQL operations

Slide 63

Slide 63 text

Apollo Introspection Query 63 63 query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } } fragment FullType on __Type { kind name description ... { "data": { "__schema": { "queryType": { "name": "QueryRoot" }, "mutationType": { "name": "Mutation" }, "subscriptionType": null, { "kind": "OBJECT", "name": "AbandonedCheckout", "description": "A checkout that was abandoned by the customer.", "fields": [ { "name": "abandonedCheckoutUrl", "description": "The URL for the buyer to recover their checkout.", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "URL", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "abandonedEmailAt", ...

Slide 64

Slide 64 text

Apollo Introspection Query 64 64 ./gradlew downloadApolloSchema \ --endpoint="https://your.domain/graphql/endpoint" \ --schema="src/main/graphql/com/example/schema.json" ./gradlew downloadApolloSchema \ --endpoint="https://your.domain/graphql/endpoint" \ --schema="app/src/main/graphql/com/example" \ --header="Authorization: Bearer $TOKEN" https://www.apollographql.com/docs/android/tutorial/02-add-the-graphql-schema/

Slide 65

Slide 65 text

Apollo Gradle Tasks 65 65 app |____Tasks | |____apollo | | |____convertApolloSchema | | |____downloadApolloSchema | | |____generateApolloSources convertApolloSchema - convert introspection query result into sdl downloadApolloSchema - download schema from GraphQL server by running introspection query generateApolloSources - code generation (part of build phase)

Slide 66

Slide 66 text

Apollo Gradle Config 66 66 apollo { service("starwars") { // Overwrite some options here for the starwars Service here if needed sourceFolder = "starwars" rootPackageName = "com.starwars" } service("shop") { // Overwrite some options here for the shop Service here if needed sourceFolder = "shop" rootPackageName = "com.shop" } // For custom scalar types like Date, map from the GraphQL type to the jvm/kotlin type. customTypeMapping = [ "DateTime" : "kotlinx.datetime.Instant", "Money" : "kotlin.String", ] } https://www.apollographql.com/docs/android/essentials/plugin-configuration/

Slide 67

Slide 67 text

Apollo Custom Scalars 67 67 GraphQL comes with a set of default scalar types out of the box: ● Int: A signed 32‐bit integer. ● Float: A signed double-precision floating-point value. ● String: A UTF‐8 character sequence. ● Boolean: true or false. ● ID: The ID scalar type represents a unique identifier. The ID type is serialized in the same way as a String. https://graphql.org/learn/schema/#scalar-types

Slide 68

Slide 68 text

Apollo Custom Scalars 68 68 GraphQL comes with a set of default scalar types out of the box: ● Int: A signed 32‐bit integer. ● Float: A signed double-precision floating-point value. ● String: A UTF‐8 character sequence. ● Boolean: true or false. ● ID: The ID scalar type represents a unique identifier. The ID type is serialized in the same way as a String. Custom scalars: """The `Date` scalar type represents date format.""" scalar Date """URL""" scalar URL https://graphql.org/learn/schema/#scalar-types

Slide 69

Slide 69 text

Apollo Custom Scalars 69 69 apollo { customTypeMapping = [ "DateTime" : "java.util.Date", "Money" : "kotlin.String", "Decimal" : "kotlin.String", "URL" : "kotlin.String", ] } https://www.apollographql.com/docs/android/essentials/custom-scalar-types/

Slide 70

Slide 70 text

Apollo Custom Scalars 70 70 val dateCustomTypeAdapter = object : CustomTypeAdapter { override fun decode(value: CustomTypeValue<*>): Date { return try { DATE_FORMAT.parse(value.value.toString()) } catch (e: ParseException) { throw RuntimeException(e) } } override fun encode(value: Date): CustomTypeValue<*> { return GraphQLString(DATE_FORMAT.format(value)) } } ApolloClient.builder() .serverUrl(serverUrl) .addCustomTypeAdapter(CustomType.DATE, dateCustomTypeAdapter) .build() https://www.apollographql.com/docs/android/essentials/custom-scalar-types/

Slide 71

Slide 71 text

Apollo Client 71 71 val apolloClient = ApolloClient.builder() .serverUrl("https://graphql.myshopify.com/api/2019-07/graphql.json") .okHttpClient(okHttpClient) .build() val response = apolloClient.query(ShopQuery()).await() if (!response.hasErrors) { println(response.data?.shop.name) } https://www.apollographql.com/docs/android/tutorial/04-execute-the-query/

Slide 72

Slide 72 text

Apollo Http Cache 72 72 // Directory where cached responses will be stored val file = File(cacheDir, "apolloCache") // Size in bytes of the cache val size: Long = 1024 * 1024 // Create the http response cache store val cacheStore = DiskLruHttpCacheStore(file, size) // Build the ApolloClient val apolloClient = ApolloClient.builder() .serverUrl("/") .httpCache(ApolloHttpCache(cacheStore)) .okHttpClient(okHttpClient) .build() https://www.apollographql.com/docs/android/essentials/http-cache/

Slide 73

Slide 73 text

Apollo Http Cache 73 73 // Control the cache policy val query = ShopQuery() val dataResponse = apolloClient.query(query) .httpCachePolicy(HttpCachePolicy.CACHE_FIRST*) .toDeferred() .await() * NETWORK_ONLY, CACHE_ONLY(expireTimeout), CACHE_FIRST(expireTimeout), NETWORK_FIRST(expireTimeout) https://www.apollographql.com/docs/android/essentials/http-cache/

Slide 74

Slide 74 text

Apollo Normalized Cache 74 74 query AllHeroes { heroes { id name friends { id name } } } { "heroes": [ { "id": "100", "name": "R2-D2", "friends": [ { "id": "200", "name": "Luke Skywalker" }, { "id": "300" "name": "Han Solo" } ] }, { "id": "300", "name": "Han Solo", "friends": [ { "id": "200", "name": "Luke Skywalker" }, { "id": "100", "name": "R2-D2" } ] } ] }

Slide 75

Slide 75 text

Apollo Normalized Cache 75 75 query SomeHero { hero(id:100) { id name friends { id name } } } { "hero": { "id": "100", "name": "R2-D2", "friends": [ { "id": "200", "name": "Luke Skywalker" }, { "id": "300" "name": "Han Solo" } ] } } CACHE_ONLY

Slide 76

Slide 76 text

Apollo Normalized Cache 76 76 NormalizedCache { "100" : { "__typename" : "Droid" "id" : "100" "name" : "R2-D2" "friends" : [ CacheRecordRef(200) CacheRecordRef(300) ] } "200" : { "__typename" : "Human" "id" : "100" "name" : "Luke Skywalker" } "300" : { "__typename" : "Human" "id" : "100" "name" : "Han Solo" "friends" : [ CacheRecordRef(200) CacheRecordRef(100) ] } "QUERY_ROOT" : { "heroes" : [ CacheRecordRef(100) CacheRecordRef(300) ] "hero({"id":"100"})" : CacheRecordRef(100) } }

Slide 77

Slide 77 text

Apollo Normalized Cache 77 77 val resolver: CacheKeyResolver = object : CacheKeyResolver() { override fun fromFieldRecordSet( field: ResponseField, recordSet: Map ): CacheKey { return CacheKey.from(recordSet["id"] as String) } override fun fromFieldArguments( field: ResponseField, variables: Operation.Variables ): CacheKey { return CacheKey.from(field.resolveArgument("id", variables) as String) } } val apolloClient = ApolloClient.builder() .serverUrl("https://...") .normalizedCache(cacheFactory, resolver) .build() https://www.apollographql.com/docs/android/essentials/normalized-cache/

Slide 78

Slide 78 text

Apollo Normalized Cache 78 78 query SomeHero { hero(id:100) { id name } } query AllHeroes($cursor: String) { heroes(after: $cursor) { pageInfo { hasNextPage } edges { cursor node { id name } } } } NormalizedCache { .... "QUERY_ROOT" : { "heroes({"after":"xxx"})" : [ CacheRecordRef(100) CacheRecordRef(200) ] "heroes({"after":"yyy"})" : [ CacheRecordRef(300) ] "hero({"id":"100"})" : CacheRecordRef(100) "100" : { ... } } }

Slide 79

Slide 79 text

Apollo Future 3.x 79 ● 100% Kotlin ● Coroutine based API ● New code generator (fragments as interface, fragments as data classes) ● KMP ● Normalized cache improvements

Slide 80

Slide 80 text

GraphQL as any other tool has own pros and cons and not a silver bullet. 80

Slide 81

Slide 81 text

Q&A 81

Slide 82

Slide 82 text

Resources 82 ● https://graphql.org/learn/ ● https://spec.graphql.org/ ● https://www.shopify.ca/partners/blog/getting-started-with-graphql ● https://github.com/VerachadW/kraph ● https://github.com/apollographql/apollo-android ● https://www.apollographql.com/docs/android/essentials/get-started-kotlin/ ● https://www.apollographql.com/ ● https://github.com/graphql/graphiql

Slide 83

Slide 83 text

83 Thanks! https://www.shopify.ca/careers #kotlinlang #androidstudygroup