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

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

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

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