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

GraphQL для мобильных разработчиков

GraphQL для мобильных разработчиков

F85e1e0cf3386c2705b84787b74d90b3?s=128

Android Broadcast

April 07, 2021
Tweet

Transcript

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

  2. Introduction 01 Agenda 2 Wait… REST? 02 GraphQL Silver bullet?

    03 GraphQL on clients 04 Apollo GraphQL client 05
  3. 3 Introduction 01

  4. • A query language for your API • Programming language

    • Developed by Facebook (2012, open sourced 2015) • GraphQL Foundation part of Linux Foundation (2018) GraphQL 4
  5. • 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/
  6. Queries 6 query ShopInfo { shop { name description }

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

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

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

    } { "data": { "shop": { "name": "graphql", "description": "An example shop." } } } https://graphql.org/learn/queries/
  10. Queries 10 query Products($after: String!, $pageSize: Int!) { products(after:$after, first:

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

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

    $pageSize) { edges { product: node { title } } } } https://graphql.org/learn/queries/ alias
  13. 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/
  14. 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
  15. 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
  16. 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
  17. Queries 17 query ProductDetails { node(id: "gid://shopify/Product/3665442689") { ... on

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

    Product { id title } } } https://graphql.org/learn/queries/#inline-fragments
  19. 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
  20. Mutations 20 mutation UpdateCustomer( $accessToken: String!, $customerUpdate: CustomerUpdateInput! ) {

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

    customerUpdate( customerAccessToken: $accessToken, customer: $customerUpdate ) { customer { firstName lastName } } } https://graphql.org/learn/queries/#mutations
  22. 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
  23. Mutations 23 mutation UpdateCustomer( $accessToken: String!, $customerUpdate: CustomerUpdateInput! ) {

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

    customerUpdate( customerAccessToken: $accessToken, customer: $customerUpdate ) { customer { firstName lastName } } } https://graphql.org/learn/queries/#mutations
  25. 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
  26. Subscriptions 26 subscription RepoComments($repo: String!) { commentAdded(repoFullName: $repo) { id

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

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

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

  30. 30 Wait... REST? 02

  31. GET /admin/api/2021-04/collections/{collection_id}.json HTTP/1.1 200 OK { "collection": { "id": 841564295,

    "products_count": 1, "title": "IPods", "body_html": "<p>The best selling ipod ever</p>", "image": { "width": 123, "height": 456, "src": "https://cdn.shopify.com/s/files/1/collections/ipod_nano_8gb.jpg" } } } REST 31
  32. • Server always in charge • Response data structure fixed

    • Over/under fetching • N+1 problem * • Schemaless * ( OpenAPI) REST 32
  33. • CORBA • SOAP • RPC / XML RPC /

    JSON RPC / gRPC • etc. Others 33
  34. 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/
  35. 35 Silver bullet? 03

  36. • 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
  37. • 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
  38. 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" } ] } ] }
  39. • 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
  40. 40 GraphQL on client 04

  41. 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": { ... }, }
  42. GraphQL On client 42 • REST way * • Unsafe

    GraphQL operation builder • Schema first codegen • Query first codegen
  43. GraphQL REST way 43 fun buildCollectionsQueryString(): String { return """

    { "query": "query { collections(first:${Params.PAGE_SIZE}) { edges { node { id title } } } }" } """.trimIndent() }
  44. 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<CollectionsGraphQLResponse> } retrofitService.getCollection( buildCollectionsQueryString() .toRequestBody("text/plain".toMediaTypeOrNull()) )
  45. GraphQL REST way 45 Pros: • Straightforward • No extra

    dependencies Cons: • Manual • Unsafe • Non scalable
  46. 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
  47. GraphQL Unsafe builder 47 Pros: • Straightforward • Kotlin DSL

    builder for GraphQL request payload Cons: • Manual • Unsafe • Non scalable
  48. GraphQL On client 48 • Document • Executable definitions ◦

    Operation definitions: query, mutation, subscription ◦ Fragment definitions • Type system (IDL / SDL) ◦ Type system definitions ◦ Type system extensions
  49. 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! }
  50. 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 } } }
  51. 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.") }
  52. 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
  53. 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<Heroes>? ) : Query.Data { data class Heroes( val name: String, val appearsIn: List<Episode>, val friends: List<Friends> ) { 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 }
  54. GraphQL Query first codegen 54 object HeroDetails_ResponseAdapter : ResponseAdapter<HeroDetails.Data> {

    val RESPONSE_NAMES: List<String> = listOf("heroes") ... object Heroes : ResponseAdapter<HeroDetails.Data.Hero> { val RESPONSE_NAMES: List<String> = 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 } } ...
  55. 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
  56. 56 Apollo GraphQL client 05

  57. 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.
  58. 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
  59. 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
  60. Apollo machinery 60 60 Gradle Plugin Compiler module Api module

    Compiler Configuration <uses> Apollo-core HttpCache NormalizedCache CoroutineSupport AndroidSupport RxSupport Runtime Runtime module
  61. 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/
  62. 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
  63. 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", ...
  64. 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/
  65. 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)
  66. 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/
  67. 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
  68. 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
  69. 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/
  70. Apollo Custom Scalars 70 70 val dateCustomTypeAdapter = object :

    CustomTypeAdapter<Date> { 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/
  71. 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/
  72. 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/
  73. 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/
  74. 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" } ] } ] }
  75. 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
  76. 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) } }
  77. Apollo Normalized Cache 77 77 val resolver: CacheKeyResolver = object

    : CacheKeyResolver() { override fun fromFieldRecordSet( field: ResponseField, recordSet: Map<String, Any> ): 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/
  78. 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" : { ... } } }
  79. Apollo Future 3.x 79 • 100% Kotlin • Coroutine based

    API • New code generator (fragments as interface, fragments as data classes) • KMP • Normalized cache improvements
  80. GraphQL as any other tool has own pros and cons

    and not a silver bullet. 80
  81. Q&A 81

  82. 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
  83. 83 Thanks! https://www.shopify.ca/careers #kotlinlang #androidstudygroup