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

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

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

Avatar for Android Broadcast

Android Broadcast

April 07, 2021
Tweet

More Decks by Android Broadcast

Other Decks in Programming

Transcript

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

    03 GraphQL on clients 04 Apollo GraphQL client 05
  2. • A query language for your API • Programming language

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

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

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

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

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

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

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

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

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

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

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

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

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

    customerUpdate( customerAccessToken: $accessToken, customer: $customerUpdate ) { customer { firstName lastName } } } https://graphql.org/learn/queries/#mutations
  23. 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
  24. 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
  25. • Server always in charge • Response data structure fixed

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

    JSON RPC / gRPC • etc. Others 33
  27. 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/
  28. • 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
  29. • 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
  30. 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" } ] } ] }
  31. • 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
  32. 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": { ... }, }
  33. GraphQL On client 42 • REST way * • Unsafe

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

    { "query": "query { collections(first:${Params.PAGE_SIZE}) { edges { node { id title } } } }" } """.trimIndent() }
  35. 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()) )
  36. GraphQL REST way 45 Pros: • Straightforward • No extra

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

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

    Operation definitions: query, mutation, subscription ◦ Fragment definitions • Type system (IDL / SDL) ◦ Type system definitions ◦ Type system extensions
  40. 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! }
  41. 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 } } }
  42. 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.") }
  43. 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
  44. 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 }
  45. 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 } } ...
  46. 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
  47. 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.
  48. 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
  49. 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
  50. Apollo machinery 60 60 Gradle Plugin Compiler module Api module

    Compiler Configuration <uses> Apollo-core HttpCache NormalizedCache CoroutineSupport AndroidSupport RxSupport Runtime Runtime module
  51. 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/
  52. 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
  53. 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", ...
  54. 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/
  55. 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)
  56. 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/
  57. 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
  58. 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
  59. 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/
  60. 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/
  61. 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/
  62. 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/
  63. 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/
  64. 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" } ] } ] }
  65. 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
  66. 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) } }
  67. 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/
  68. 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" : { ... } } }
  69. Apollo Future 3.x 79 • 100% Kotlin • Coroutine based

    API • New code generator (fragments as interface, fragments as data classes) • KMP • Normalized cache improvements
  70. 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