GraphQLにPOSTをしてみる

GraphQLのリクエストをapollo-androidとRetrofitでそれぞれ実装してみました。

github.com

なのだけれど、実装中にGitHubのGraphQLドキュメントを読んでたら大半書いてあったので、多分こっちを隅から隅まで読んでもらう方がいいブログです。 やっぱり公式ドキュメントとかデファクトスタンダードのライブラリドキュメントを読むの大事。

docs.github.com

GraphQL

graphql.org

2020年のAndroid/iOSアプリ開発だと、長くメンテしているアプリだとなかなか置き換えることができませんが、GraphQLがきてます。TwitterGitHubも、どちらも下記のようなブログ出しているあたりにキテる感をみてます。*1

blog.twitter.com

github.blog

もちろんみんな大好きFacebook Loginを導入すると、GraphQLのリクエストをすることになります。 もしかすると、意図せず「あのレスポンスからキー名を指定して必要なパラメーターを取得しているアレ」と見ているやつかもしれません。

developers.facebook.com

きっかけ

GraphQLをアプリに導入しようとすると、大体次の記事にたどり着きます。

employment.en-japan.com

実際、筆者のプロダクトでも、最初のGraphQLエンドポイントの追加から1年近くたってようやく専用クライアントのApolloを導入したのでした。それまでは、単にJSONをPOSTしてJSON responseを受け取るAPI endpointがひとつ増えただけという運用でした。

自分の環境の場合、導入当初はGraphQLに順々に移行していこう、という話で始まったので最初からApolloを導入しました。

github.com

github.com

しかし、諸事情ありGraphQLで運用されるAPIが増やせないまま1年半ほど経ってしまいました。 Apollo-AndroidApollo-iOSは開発が非常に早く、また急激に進んでいくため、メンテナンスやバージョンアップ時のコストが高いライブラリの印象があります。 このこともあり、一時的に脱Apolloをすることとしました。

Apolloは、元々GraphQLをスキーマによるクラス定義程度しか利用していなかったため、簡単に行うことができました。ApolloSQLを利用したキャッシュなどをガッツリ使っている場合には、簡単には行かないと思うので、要件的にラッキーだったと思います。 ただこの呼び替えを実装するにあたり「GraphQLのエンドポイントにRetrofit/URLRequestを利用してリクエストを送る」実装の例が見当たらず、100%手探りの対応をすることになりました。

そんなわけで、やったことの復習がてらサンプルアプリを作りました。

やったこと

GitHub APIApollo-AndroidとRetrofitから同じ(この表現が難しい)レスポンスを取得するリクエストを投げるサンプルです。*2

GraphQLRepository

GraphQLをApollo-Androidを用いて呼び出しています。

class GraphQLRepository @Inject constructor(
    private val client: ApolloClient
) {
    suspend fun repositories() = client
        .query(RepositoriesQuery())
        .await()
        .data?.viewer?.repositories?.nodes
        .orEmpty()
        .filterNotNull()
}

これと

query Repositories {
  viewer {
    repositories(first: 20) {
      nodes {
        ...RepositoryFragment
      }
    }
  }
}

fragment RepositoryFragment on Repository {
  id
  name
  url
  description
  createdAt
}

のクエリの組み合わせです。 アクセストークンを生成した「自分の」リポジトリを上から20取得します。

なお、Apolloでは Repositories のようにクエリに名前をつける必要があるため query Repositories となります。 ここで自動生成されたコードがどうなるかは……ちょっと長くなるのでcloneして実行してみていただければ。

RetrofitRepository

呼び出しの処理をひとまとめにすると、下記のようになっています。

class RetrofitRepository @Inject constructor(
    private val service: RetrofitService
) {
    suspend fun repositories() = service.repositories(
        GraphQLBody(
            query = "query { viewer { repositories(first: 20) { nodes { ...RepositoryFragment } } } } fragment RepositoryFragment on Repository { id name url description createdAt }",
            variables = "{}"
        )
    ).data.viewer.repositories.nodes
}
interface RetrofitService {

    @POST(value = "graphql")
    suspend fun repositories(@Body body: GraphQLBody): RepositoryResponse
}
@JsonClass(generateAdapter = true)
data class GraphQLBody(
    val query: String,
    val variables: String
)

こちらは先述のクエリからクエリに付けられた名前を削除したものとなります。 GraphiQLあたりで作成したクエリをそのまま貼り付けるとこうなる感じですね。

fragment として定義したRepositoryFragmentをどうやって受け取るかというと、下記のようなJSONをパースするコードが必要になります。

@JsonClass(generateAdapter = true)
data class RepositoryResponse(
    val data: RepositoryData
)

@JsonClass(generateAdapter = true)
data class RepositoryData(
    val viewer: RepositoryViewer
)

@JsonClass(generateAdapter = true)
data class RepositoryViewer(
    val repositories: RepositoryNodes
)

@JsonClass(generateAdapter = true)
data class RepositoryNodes(
    val nodes: List<RepositoryFragment>
)

@JsonClass(generateAdapter = true)
data class RepositoryFragment(
    val id: String,
    val name: String,
    val url: String,
    val description: String?,
    val createdAt: String,
)

シンプルではあるのですが、毎回書くとちょっと嫌な気持ちになる量なことが伺えるかなと思っています。

簡単なまとめ

GraphQLで多数のリクエストを送るような処理、もしくは頻繁にデータ構造を変えたいケースではApolloなどのライブラリを利用したくなることが伺えるかなと思っています。 依存するライブラリを削減したかったり、一部からGraphQLを導入する際にはリポジトリを参考にしてもらえれば。

なお、サンプルアプリを動かすにはRepositoryへのアクセス権限を持つPersonal Access Tokenが必要になります。 GitHubのDeveloper Settingsから生成してみてください。

*1:個人的にはgRPCにもっと期待してます

*2:この文章書いてたら、別に作らなくてもよかったのではという気もしてくる