Apollo Client Kotlin チュートリアルざっくりまとめ

### 概要

本記事では、Apollo GraphQL Kotlin Clientの概要と使い方について紹介します。特に、以下のチュートリアルについてまとめます。

[Introduction to Apollo Kotlin Tutorial](https://www.apollographql.com/docs/kotlin/tutorial/00-introduction/))
なお、本記事はチュートリアルを踏まえた情報のまとめとなっております。このため、チュートリアルを読まれることを推奨します。

### Apollo Kotlinとは
Apollo GraphQL Kotlin Clientは、GraphQLクライアントです。GraphQLに対応したAPIの場合、単一のエンドポイントから必要なデータを柔軟に取得できます。さらに、設定ファイル .graphqls を用いることで、クライアントから使用するモデルクラスを自動生成してくれます。このため、JSONのパースやモデルクラスへのマッピングを行う必要がなく、開発効率が向上します。当然ながら、APIがGraphQLに対応している必要があります。

詳しくは、[公式GitHubリポジトリ](https://github.com/apollographql/apollo-kotlin#apollo-kotlin)を参照してください。

#### GraphQLとは

GraphQLは、Web APIの設計手法の1つです。従来のREST APIに比べ、以下のような特徴があります。

| GraphQL                                                           | REST API                                                                                  | 備考                                             | 
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------ | 
| データのフェッチに必要なリクエスト数が少なくなる	                 | 複数のエンドポイントが必要なため、データのフェッチに必要なリクエスト数が増える<br>        | 「アンダーフェッチがない」って言い方をしたりする | 
| 必要なデータだけを取得できるため、レスポンスサイズが小さくなる    | 一度に大量のデータを取得することができるため、レスポンスサイズが大きくなる可能性がある    | 「オーバーフェッチがない」って言い方をしたりする | 
| 単一エンドポイントで複数のデータを要求できる	                     | エンドポイントごとに異なるURLを使用する必要があり、複数のリクエストが必要になる           |                                                  | 
| APIから必要なデータを柔軟に取得できる	                            | APIから取得できるデータは事前に定義されており、必要なデータをすべて取得する必要がある<br> |                                                  | 
| サーバー側で定義されたスキーマに基づいて動的にデータを要求できる	 | APIエンドポイントが変更された場合、コード内のエンドポイント情報も変更する必要がある<br>   |                                                  | 

シンプルにまとめると「GraphQLは柔軟で、効率的で、使いやすいAPIを提供するために設計されている。」といったイメージです。

**データ授受のイメージ**

APIのリクエストとレスポンスの形式を所定のファイルに定義するとJsonが返ってきます

```javascript
// イメージ
{
  hero {
    name
  }
}
```
↓  以下形式でJsonレスポンスが取得
```json
{
  "hero": {
      "name": "Luke Skywalker"
  }
}
```

フィールドを追加すると必要な情報だけ取ってこれる。
```json
{
  hero {
    name
    height
  }
}
↓
// json
{
  "hero": {
      "name": "Luke Skywalker",
      "height": 1.72,
  }
}
```

**GraphQLクライアントはここからさらにModelクラスをつくってくれる**
所定のファイル = graphqls ファイルでスキーマを定義、graphqlで実行クエリを定義
```javascript
// hoge.graphqls
type SampleQuery {
    hero: Hero
}

type Hero {
    name: String!
    height: Int!
}

schema {
    query: SampleQuery
}

// hoge.graphql
query getHero {
    hero {
        name
    }
}
```
↓ 
```java
//
// AUTO-GENERATED FILE. DO NOT MODIFY.
//
// This class was automatically generated by Apollo GraphQL version '3.7.5'.
//
package com.example.rocketreserver

import ...

public class GetHeroQuery() : Query<GetHeroQuery.Data> {
  public override fun equals(other: Any?): Boolean = other != null && other::class == this::class

  public override fun hashCode(): Int = this::class.hashCode()

  public override fun id(): String = OPERATION_ID

  public override fun document(): String = OPERATION_DOCUMENT

  public override fun name(): String = OPERATION_NAME

  public override fun serializeVariables(writer: JsonWriter,
      customScalarAdapters: CustomScalarAdapters): Unit {
    // This operation doesn't have any variable
  }

  public override fun adapter(): Adapter<Data> = GetHeroQuery_ResponseAdapter.Data.obj()

  public override fun rootField(): CompiledField = CompiledField.Builder(
    name = "data",
    type = SampleQuery.type
  )
  .selections(selections = GetHeroQuerySelections.__root)
  .build()

  @ApolloAdaptableWith(GetHeroQuery_ResponseAdapter.Data::class)
  public data class Data( // Model データ
    public val hero: Hero?,
  ) : Query.Data

  public data class Hero( // Model データ
    public val name: String,
  )

  public companion object {
    public const val OPERATION_ID: String =
        "b15cc8a0721be79e5311944346bb73770d1c84d0a0cac560e2b69c29cb993e7b"

    /**
     * The minimized GraphQL document being sent to the server to save a few bytes.
     * The un-minimized version is:
     *
     * query getHero {
     *   hero {
     *     name
     *   }
     * }
     */
    public val OPERATION_DOCUMENT: String
      get() = "query getHero { hero { name } }"

    public const val OPERATION_NAME: String = "getHero"
  }
}
```

`GetHeroQuery.Data` 型のModelクラスからHeroが取れる。

#### GraphQL 用語
[Tutorial](https://www.apollographql.com/docs/kotlin/tutorial/00-introduction/) に出てくる用語をまとめると

**スキーマ定義**
- Objects
GraphQLスキーマに定義するオブジェクトのタイプです。クライアントがフィールドを指定するために使用されます。
- Scalars
GraphQLに組み込まれているスカラータイプ。Int、String、Float、Boolean、IDが含まれます。カスタムスカラーを定義することもできます。

- Enums
GraphQLスキーマに定義する列挙型です。列挙型は、GraphQLスキーマに対して固定された値を持つフィールドを作成するために使用されます。


**クエリの実行**

- Query
クライアントがデータを取得するために使用するメソッドです。フィールドの一覧を指定して、返されるデータの構造を定義します。 （RestでいうGet

- Mutation
クライアントがデータを変更するために使用するメソッドです。クライアントが送信する変更を指定して、返されるデータの構造を定義します。 （RestでいうPostやPatch

- Subscription
クライアントがデータの変更を受け取るために使用するメソッドです。リアルタイムで更新されるデータを取得するために使用されます。 （WebSockts


**スキーマ定義に関する特殊な用語**

- Directives
スキーマの実行方法に影響を与える指令です。例えば、特定のフィールドを省略したり、フィールドを変形したり、特定のスキーマのフィールドのアクセスを制限することができます。


### サンプル実装
必要なことをたらたら書きます。

まずgradle

```gradle
plugin {
    id("com.android.application")
    // ...
    id("com.apollographql.apollo3").version("anyversion")
}

apollo {
    service("service") {
        packageName.set("com.example.rocketreserver") // package名を指定する
    }
}

dependencies{
....
  implementation("com.apollographql.apollo3:apollo-runtime:anyversion")
}

```
前述の設定ファイルであるhoge.graphqls, hoge.graphql（ [サンプル](https://github.com/apollographql/apollo-kotlin-tutorial/tree/main/compose/final/app/src/main/graphql)　） は以下フォルダに格納します。
その位置を示すため上記ではパッケージ名を指定してます。
`{module}/src/main/graphql`


クライアントクラスはOkHttpのお作法と同じです。

```kotlin
val apolloClient = ApolloClient.Builder()
    .serverUrl("https://apollo-fullstack-tutorial.herokuapp.com/graphql")
    .webSocketServerUrl("wss://apollo-fullstack-tutorial.herokuapp.com/graphql")
    .okHttpClient(
        OkHttpClient.Builder()
            .addInterceptor(AuthorizationInterceptor())
            .build()
    )
    .webSocketReopenWhen { throwable, attempt ->
        Log.d("Apollo", "WebSocket got disconnected, reopening after a delay", throwable)
        delay(attempt * 1000)
        true
    }

    .build()

class AuthorizationInterceptor() : Interceptor ... 
```
[コード](https://github.com/apollographql/apollo-kotlin-tutorial/blob/main/compose/final/app/src/main/java/com/example/rocketreserver/Apollo.kt)


呼び方
- Queryリクエスト

```kotlin
response = apolloClient.query(LaunchListQuery(Optional.present(cursor))).execute()
```
`LaunchListQuer`は[graphql](https://github.com/apollographql/apollo-kotlin-tutorial/blob/main/compose/final/app/src/main/graphql/LaunchList.graphql)から作られたQuery実行用のクラス。
`Optional.present(cursor)` はリクエスト時に設定するパラメータ。

レスポンス型 ->  `ApolloResponse<LaunchListQuery.Data>?`

- Mutationリクエスト

```kotlin
apolloClient.mutation(LoginMutation(email = email)).execute()
```
`LoginMutation`は[graphql](https://github.com/apollographql/apollo-kotlin-tutorial/blob/main/compose/final/app/src/main/graphql/Login.graphql)から作られたMutation実行用のクラス。
レスポンス型 -> `ApolloResponse<LoginMutation.Data>`


- Subscription

```kotlin
val tripBookedFlow = remember { apolloClient.subscription(TripsBookedSubscription()).toFlow() }
    val tripBookedResponse: ApolloResponse<TripsBookedSubscription.Data>? by tripBookedFlow.collectAsState(initial = null)
    LaunchedEffect(tripBookedResponse) {
        if (tripBookedResponse == null) return@LaunchedEffect
        val message = when (tripBookedResponse!!.data?.tripsBooked) {
            null -> "Subscription error"
            -1 -> "Trip cancelled"
            else -> "Trip booked! 🚀"
        }
        // TODO use the message
    }
```

subscription なのでFlowで変更を監視するのが一般的
`TripsBookedSubscription`は[graphql](https://github.com/apollographql/apollo-kotlin-tutorial/blob/main/compose/final/app/src/main/graphql/TripsBooked.graphql)から作られたMutation実行用のクラス。


### おわり
SQLDelightも設定ファイルからModelクラス作成までやってくれるので、マルチプラットフォーム環境だとこれがあたりまえなのか。
仕事柄モバイルに閉じこもってますが、外の世界もおもしろいですね。