Continuity is The Father of Success

Androidアプリとかゲームとか。毎日続けてるものについて。

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:この文章書いてたら、別に作らなくてもよかったのではという気もしてくる

Danger-JS + Android Lintの話

Danger-JSにAndroidLintを動かすプラグインがぱっと見なさそうだったので、TSの勉強も兼ねて作ってみました。

github.com

作る中で学んだこととか、考えたことなどをまとめておきます。

Danger

Dangerを全く知らない人は、下記ブログをざっと読むのをお勧めします。 日本語記事もたくさんありますし、GitHubに参考実装もたくさんあるので、読まなくとも動かすならなんとでもなります。Dangerの文脈把握にはブログが一番、ツールは文脈把握が一番、みたいな思想です。

artsy.github.io

なお、日本語ブログだとコネヒトさんのブログがざっくり概要をつかむのに適している気がしました。

tech.connehito.com

実装についてはは、おおよそGitHubやBitbucketへのAPIアクセスをラップし、メッセージや警告をPR上にコメントしてくれるラッパーライブラリと捉えています。 そして、ありがたいことにプラグインにより様々な状況に対応することができます。*1

2つのDanger

さて、そんなDangerですがRubyとJSの2つの実装が存在します。*2

それぞれの比較は下記のページから。

danger.systems

大きな違いというとそこまで違いはないのですが、

Danger on Ruby is more mature, has a solid plugin eco-system and is very stable.

とあるようにRuby版の方が安定しています。 実際に今踏み抜いて困っているのが、Danger-JS依存で発生する「同一ファイルに複数のインラインコメントをつけようとするとエラーが起きる」問題です。

github.com

上記IssueはSwift版のリポジトリに建てられているIssueなのですが、Danger-JSのみを利用しているプロジェクトでも発生しているため、Danger-JS由来の問題の様子です。 pull_request_review_id must be pending エラーの説明が特にないので、色々と困る。

ざっとRubyとJSのコードを見比べてみたのですが、多分きっとおそらく、PRへのコメント時にRuby側が1つのリクエスJSONにまとめている箇所が、JSでは Promise.all しているために発生している……ような……雰囲気です。 再現できる処理は見つかっているので、時間のある時に差し障りのない感じで再現リポジトリ作ってみようかなと思ってます。*3 ※再現するリポジトリ作って*4、issueで報告しました*5

そのほか、Ruby版は github.dismiss_out_of_range_messages をDangerfile内で書き込むことで変更がない点へのコメントを控えますが、JS版は--ignoreOutOfDiffComments オプションを実行時に付与するようです。 細か〜な違いが、多分他にもあるはず。

Android LintとDanger

GitHubであれば、PRのDescriptionであったりタグであったり、変更総行数であったりに対する messagewarning の独自のルールを策定する場合には Dangerfile (JS版なら dangerfile.jsdangerfile.ts)内で github オブジェクトをこねこねすれば事足ります。 しかし、せっかくPRにコメントの形で指摘が記載できるのであれば、Lintのチェックもしたくなるのが人情です。 「不要なimportが含まれています」とか「変数名はsnake_caseにしてください」なんかを指摘したくないですよね。そんなことは機械に任せたい。

最近忘れがちになるのですが、自分はAndroidエンジニアなのでAndroid Lintを走らせて指摘させたくなりがちです。 ざっと調べてみたところ、RubyのDangerに対応するpluginはある*6ものの、JSのDangerに対応するpluginは見つかりませんでした。 そんなわけで、作っていきます。

プラグイン開発

Danger-JSのプラグインを作成することは、npmにJSの(TSの)ライブラリを登録することになります。 package.json に依存関係を書くことで、プラグインを導入し実行するイメージです。

以下、やりたいことを列挙してみます

  • danger.js (danger.ts)で1~2行書けばAndroid Lintの実行とインラインコメントがなされるようにしたい
    • 最初は app の決め打ちで良さそうだが、変更できるようにしておいた方がよさそう
    • 最初はインラインコメントの決め打ちで良さそうだが、変更できるようにしておいた方がよさそう
  • TSでコードは書きたい(型情報をちゃんと持たせたい)
    • とりあえず開発時の最新バージョンを使いたい

Android Lintを実行する

CI上でAndroid Lintを走らせたいということは、CIで gradlew lint を実行すればいいことになります。 幸いNode.jsには execSync メソッドによりコマンドを実行できるので、execSync('gradlew lint')を動くように出来ればよさそうです。

たまたまメンテナンスをしていた自分のOSSライブラリで適当に実行してみると、ファイルパスに問題があることがわかってきます。

Run yarn danger ci --ignoreOutOfDiffComments
  yarn danger ci --ignoreOutOfDiffComments
  shell: /bin/bash -e {0}
  env:
    GITHUB_TOKEN: ***
yarn run v1.22.5
$ /home/runner/work/EmptyRecyclerView/EmptyRecyclerView/node_modules/.bin/danger ci --ignoreOutOfDiffComments
/bin/sh: 1: gradlew: not found

Error:  Error: Command failed: gradlew lint --no-deamon
/bin/sh: 1: gradlew: not found

    at checkExecSyncError (child_process.js:616:11)
    at Object.execSync (child_process.js:652:15)
    at Object.<anonymous> (/home/runner/work/EmptyRecyclerView/EmptyRecyclerView/node_modules/dangerjs-android-plugin/dist/index.js:35:29)
    at Generator.next (<anonymous>)
    at /home/runner/work/EmptyRecyclerView/EmptyRecyclerView/node_modules/dangerjs-android-plugin/dist/index.js:8:71
    at new Promise (<anonymous>)
    at __awaiter (/home/runner/work/EmptyRecyclerView/EmptyRecyclerView/node_modules/dangerjs-android-plugin/dist/index.js:4:12)
    at Object.androidlint (/home/runner/work/EmptyRecyclerView/EmptyRecyclerView/node_modules/dangerjs-android-plugin/dist/index.js:26:12)
    at Object.<anonymous> (dangerfile.ts:4:27)
    at Module._compile (internal/modules/cjs/loader.js:1075:30) {
  status: 127,
  signal: null,
  output: [
    null,
    <Buffer >,
    <Buffer 2f 62 69 6e 2f 73 68 3a 20 31 3a 20 67 72 61 64 6c 65 77 3a 20 6e 6f 74 20 66 6f 75 6e 64 0a>
  ],
  pid: 2969,
  stdout: <Buffer >,
  stderr: <Buffer 2f 62 69 6e 2f 73 68 3a 20 31 3a 20 67 72 61 64 6c 65 77 3a 20 6e 6f 74 20 66 6f 75 6e 64 0a>
}


Failing the build, there is 1 fail.
Feedback: https://github.com/koji-1009/EmptyRecyclerView/pull/4#issuecomment-687710583
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
##[error]Process completed with exit code 1.

*7

調べてみると process.cwd() により実行中のディレクトリが取得できるとのことなので、 const dir = process.cwd() と置いてpathを修正することで、TSからGradleを実行することができました。

Android Lintの実行結果をTSで扱う

Android LintのはHTMLやxmlで出力されます。 このため、xmlファイルをJSONとして扱う(TSのコード内の文脈でJSONと書くのが正しいのか……?)方法を探してみると、いくつかのライブラリがあることがわかってきました。今回は、StackOverflowなどでお勧めされていた xml2js を利用してみます。

www.npmjs.com

LintResult としてパースしたい型を定義し、先ほどの dirAndroid Lintがxmlを出力する先の path を組み合わせてあげます。 これでAndroid Lintの結果を扱えるようになりました。

const lintRaw = readFileSync(`${dir}/${path}`, 'utf-8')
const json = await parseStringPromise(lintRaw, { mergeAttrs: true })

const lintResult = json as LintResult

与えた型はこんな感じです。

interface LintResult {
    format: string
    by: string
    issues: Issues
}

interface Issues {
    issue: [Issue]
}

interface Issue {
    id: [string]
    severity: [string]
    message: [string]
    category: [string]
    priority: [string]
    summary: [string]
    explanation: [string]
    includedVariants: [string]
    excludedVariants: [string]
    url: [string]
    urls: [string]
    errorLine1: [string]
    errorLine2: [string]
    location: [Location]
}

interface Location {
    file: [string]
    line: [string]
    column: [string]
}

なお xml2jsの利用時に注意が必要なのは、下記2点です。

  • mergeAttrs: true を設定する
    • xml2js でパースを行うと $ によって階層が一つ深くなるようにパースされるため、 mergeAttrs オプションを有効にします
    • デフォルトの文字を変えることもできるようなのですが、階層を浅くした方が扱いやすいのではないかなと思います
  • 全ての要素を配列として扱う
    • xml2js でパースを行うと、すべての要素が配列としてパースされるのですが、これをうかつに解除すると事故がおきます
      • explicitArray: false をする時には計画的に
    • xmlJSONにパースする際、xmlの構造からは「その子要素が単数か複数か」を判断することができないためです
      • xmlの子要素が1つの時には単数に、複数の時には配列に、与えるxmlの状態に応じて型がおかしくならないように
      • 型を受け取ってパースしてくれるライブラリがあれば、もっとスムーズなのかも

Android Lintの結果を出力する

最後にDangerの message warn fail メソッドを呼び出して、PRにコメントをつけていきます。 なお fail を呼び出すとPRのstatusがfailになるので、注意が必要です。

Android Lintはコード全体のLintをかけるので、出力する先が今回の変更範囲内かどうか(少なくとも同一ファイルかどうか)をチェックしつつ、コメントをつけてみます。

const editFiles = danger.git.modified_files.filter(element => !danger.git.deleted_files.includes(element))
const createFiles = danger.git.created_files
const files = [...editFiles, ...createFiles] // 追加と編集ファイルのみに制限できるようにする

for (const issue of lintResult.issues.issue) {
    const location = issue.location[0]
    const filename = location.file[0].replace(`${dir}/`, '') // 絶対pathで出力されていた場合に、相対pathに変換
    if (!files.includes(filename)) { // メッセージを送るファイルを選別
        continue
    }

    const line = parseInt(location[0]['line'] ?? '0')
    send(issue.severity[0], issue.message[0], filename, line)
}

function send(severity: string, messageText: string, file: string, line: number) {
    switch (severity) {
        case Severity.WARNING:
            warn(messageText, file, line)
            break
        case Severity.INFORMATIONAL:
            message(messageText, file, line)
            break
        case Severity.FATAL:
        case Severity.ERROR:
            warn(messageText, file, line)
            break
        default:
          // nop
    }
}

これでメッセージの送信周りが実装できました。 なお今見てたら直したい箇所ができてきたので、このコードは執筆当時のものになる恐れがあります。

Pluginとして登録

npmにアカウントを作成し、yarn publish あたりでアップロードしましょう。 なお、ここで yarn build してから yarn publish することを失念し、数バージョンコードの更新がされるずにアップロードされてしまう問題にハマりました。 慣れてないツールは手間取りますね。

www.npmjs.com

あとは danger typescript と共に dangerjs-android-plugin を依存関係に加え、GitHub Actionsなどの任意のCIでDanger-JSを実行すれば、Android Lintが走ってくれるようになります。 デフォルト設定であれば dangerfile.ts は下記のようにしてください。 なお、この辺りも改善の余地しかないので、しばらくすると更新されREADMEをアップデートすると思います。

import { androidlint } from 'dangerjs-android-plugin'

androidlint()

お疲れ様でした。

RubyとJSのどちらをAndroidの開発で利用するのが良いのか

最後に私見です。

現状では「RubyのDangerを使うべき」ではないかなと思っています。 理由としては、下記2点です。

  1. JSよりもRubyの方が既存のビルドシステムに入っていることが多い
  2. Ruby版の方が 安定 している

一方で、JS版を試していくと下記のようなメリットがあると感じています。

  1. 楽しい
    • 未舗装の分野を進んでいる感じがするので、そもそも楽しい
    • TSで型のサポートが入るため、Rubyよりも馴染みやすい
  2. 知識の移植性が高い
    • TSをshell scriptとして利用することができるので、他の箇所や分野でも利用できそうな気になってきます
    • 様々な環境をサポートしやすい(ブログの思想より)ので、今後の活用の幅が広そうに感じています
  3. nodeを利用したツールがある場合に統合できる
    • apollo-androidを意識しています
    • GraphQLは採用事例が増えてくると思われるので、firebase-toolsとの組み合わせでなんとかするとスッキリしそうです

終わりに

8月半ばからいろんなものと並行して進めていたものが、ベータ版にまで進んだのでよかったです。 当初は「fastlaneのFirebase App Distribution Pluginにfirebase-toolsにnodeが必須だし、PRレビューをnode依存で完結されられたら、色々と効率的なのでは?」ぐらいの思いつきで始めていたので、v0.2.0でfirebse-toolsの依存が不要になった時にはずっこけそうになりました。

とりあえず動くところまで来た、ぐらいの感じなのですが、チラッと見たnpmのダウンロードがなぜか成長していたので早めに気になるところとか直していこうと思います。

プログラミングTypeScript ―スケールするJavaScriptアプリケーション開発

プログラミングTypeScript ―スケールするJavaScriptアプリケーション開発

  • 作者:Boris Cherny
  • 発売日: 2020/03/16
  • メディア: 単行本(ソフトカバー)

せっかく家にこの本あるし、この機会にちゃんと読んでおこう……。

*1:プラグインの一覧はGitHub - danger/awesome-danger: An awesome list of all things Dangerから確認できます。

*2:https://danger.systemsを見るとSwift/Kotlin/Pythonのものも存在しますが、それぞれ柄が同じDangerを各言語から呼び出せるようになっている様子です。個人的に、この「Kotlin書く人ならKotlinでDSL書きたいでしょ」というサポート思想は好きです。ただ、ある組織内で知見を共有するときにTSで統一した方が共有しやすそうなので、今回TSでプラグインを作ってみています。

*3:会社のブログ文章管理リポジトリで書いたTextLintで再現したので、適当な文章とDangerfile作ればなんとかなるはず。

*4:Create project by koji-1009 · Pull Request #1 · koji-1009/pending_pull_request_review_id · GitHub

*5:Using custom github app doesn't work well · Issue #1054 · danger/danger-js · GitHub

*6:GitHub - loadsmart/danger-android_lint: A Danger plugin for Android Lint

*7:Plugin v0.1.6. · koji-1009/EmptyRecyclerView@7a5df19 · GitHub

2020.1Qの振り返り

3ヶ月、1回の出社で終わったのでこの体験の忘備録も含めて。

生活

これに尽きる。

news.yahoo.co.jp news.yahoo.co.jp

  • ご飯
    • 下の記事を見て、23時過ぎに近くのスーパーへ麺類と冷凍肉などを買いに行ったのを覚えている r.nikkei.com
    • 缶詰を定期的に頼んで食べていたのはよかったと思う
    • 米とうどんの消費が増えて、パンの消費が減った
      • 米とうどんは備蓄できるため
      • ホームベーカリーはあったが、諸々用意することを考えると仕舞い込んだままだった
    • Amazonや某納税の受け取り程度で、ウーバーなイーツなどは頼まなかった
    • コーヒーの大量消費に備えて学園祭ぶりに珈琲問屋に発注かけた www.tonya.co.jp
      • 一方で、飲み過ぎてカフェイン抜いたときに頭痛が
  • 飲酒
    • もともと冷蔵庫に入っていなかったため、飲まなかった
    • 自粛が明けてからもオンラインな飲み会などは断ることが多かった
    • コロナ不安や自粛のストレスがあるなか、飲酒はリスクが高いように思えてしまう
  • 運動
    • 極力外出しないままに終わった
    • 散歩などは、と言われていたが、自身と周囲へのリスクを考えてほぼ外出しない生活をしていた
      • 4~5月は靴を履いた回数が5回を切るのでは、といったところ
    • リングフィットアドベンチャーを黙々とやっていた
      • 4月半ばごろから、運動不足から来る寝付きの悪さが気になりだしたことを覚えている
      • いつの間にか100レベルを越え、継続日数も100日を越えた
      • それでもスクワットはきつい、泣きたくなる
      • コインランとぽっこりお腹改善セットは絶対に許さない、絶対にだ
  • 娯楽
    • 友人たちと通話しているのが娯楽の主なところを占めていた
    • やりかけのDEATH STRANDINGやポケモンなどがあったが、モニタ疲れや気疲れであまり遊ぶ気持ちに慣れなかった
      • 正直、今でもあまり遊ぶ気持ちに慣れていないところが大きい
    • 一方でアークナイツを遊んでいる www.arknights.jp
    • FGO秋葉原でなんか色々と面倒になってしまった……
    • 少しは仕事でのモニター見過ぎが響いている気もしている
    • Helltakerは面白かった store.steampowered.com
  • 学習
    • コロナ後に何があるかわからないしなー、という気分で機械学習クラウドの学習をしていた
    • 5月はありがたいことにいろんな会社がコースを開放してくれたので助かった
    • 一方で、同時に3つぐらいの学習を進めることになり、5月はなんだかんだで余暇を使い切ってしまった

仕事

  • 4月はAndroid + iOSで、5月はiOSで、6月はiOSとFlutter(チョット)だった
    • Android
      • RoomとかFlowとか触ってかなり楽しかった
      • AndroidStudio4.0がなかなか出なかったので、beta版で開発していたのを覚えている
      • Canvasの上で円を描いたり文字書いたり、それなりに手間なコードがさっくりかけたので「書けるようになってきたなー」を思った時期でもあった
      • OkHttp BOMやFirebase BOMなどなど
    • iOS
      • NotificationCenter を駆使してチョットリアクティブなコードを書いていた
      • 世間の状況にアプリは影響されることを痛感した
      • 超富豪プログラミングは大変なことを招くと学んだ
      • SwiftPMの導入とかUbuntu上でSwiftLintを走らせるとか、GitHub Actionsを楽しく使っていた
    • Flutter
      • 元々趣味で触っていたところきっかけで、仕事でも触るようになった
      • まだまだ慣れていないところが大きい
      • plugin起因の問題をいくつか踏み抜いている、頑張っていきたい github.com
  • PM
    • モバイルアプリのテックリードということになり、サービスの開発優先度を決める会議の進行役をやるようになった
    • 正直大学生の時の進行のノリで進めている
      • 当時よりは喋れるようになっていることが進歩かも
      • やりたいことを聞くと大体何をする必要があるかわかるようになっているので、仕事の経験も積んだな〜という気分になる
    • そこそこ大きな機能開発の進行もやることになった
      • 全てオンライン上でやることになった(それしかないのだけれど)ので、チョット注意して進めた
      • とはいえ、オフラインでやる時とやるべきことは変わらないな、という印象
      • 打鍵の速さは地味にこの状況下では武器なのかもしれない
        • 正確に、配慮された文章を高速で書く必要がある時代
        • チャットの速度で疑問を表明して、相談して、整理して、結論をまとめる時代
    • ミーティングの中でちょっとした笑いが取れないかと画策している
      • いい加減な性格でよかったと思っている
  • 環境

アウトプット

  • アプリを作りかけているのだけれど、どうにも手が進まない
    • Androidのネイティブと、iOSのネイティブで作りたいなと思っている
  • ブログもなかなか書く気がしなかった
  • 娯楽も趣味も、健康が源なのだなと痛感している