AWS Japanで1年経った

4月1日でAWS Japanに入って1年が経ちました。とても早かったです。

ネットワークエンジニアとしてキャリアが始まり、自宅サーバに固定IPを8本引いてXenKVMで仮想インスタンスをホストして知人に有料で貸してたら海の向こうからやってきたEC2の登場に腰を抜かし、その後バックエンドエンジニアになり、Androidエンジニアになり、10年の時を経てAWSを使ったお仕事をしているのは感慨深いものがあります。

AWS Japanで働いていてよかったことは沢山ありますが、これまでとは比較にならない深度でAWSのことを理解し、様々な生のユースケースに触れられるところです。AWSのスキルは言ってしまえばいちクラウドベンダの知識に過ぎませんが、それが世界中で使われているので事実上世界で通用するスキルを身に着けていることになるのは素晴らしいことです。

僕のキャリアはスタートアップといわゆるウェブ系企業を渡り歩いてきたので、比較的シンプルな構成のアプリケーションに関わることが多かったですが、AWS Japanの技術者として色々なお客様と仕事をしていると、かつての自分には想像もつかなかったような難しい要件を求められることが少なくありません。その時に、どんな難しい場所にもぴったり合うブロックのピースを見つけられるのは、今なおAWSをおいて他にないというべき境地にあると感じます。

21世紀も20年代に突入し、エンジニアとクラウドの関わり方もまた少しずつ変わりつつあります。今年はぜひとも、自分の最大の強みであるモバイル開発の観点からもAWSの良さを伝えていけたらなと考えています。今年も宜しくお願いいたします!

そうそう、僕のチームは次の2ポジションを熱烈募集しています。ご興味のお有りの方は是非お声がけください!それでは。

www.amazon.jobs

www.amazon.jobs

心のローパスフィルタ

地殻を毛細血管のごとく駆け巡る光ファイバが空間の垣根を取り去った結果、四半世紀前なら触れることすらなかった情報に晒されるようになった。 これは幸福でも不幸でもある。

観測する限り、多くの人は自分の置かれている環境の平均値で構成されている。朱に交われば赤くなるというやつだ。 これは環境のお陰で自分が周りのレベルまで引き上げられる例も、自分自身がいわば浸透圧の合った塩分濃度に自然と落ち着く例もあるだろう。 とにかく、環境から著しく外れた存在に触れるには周囲の人間を何ホップもしなければならなかった。

ところがインターネットが魔界の界峡トンネルのように突然1ホップで次元の違う人々に出会うことを可能にしてしまった。その結果、ブラウザ越しの玄関先にはS級妖怪がゴロゴロ歩いている。テクノロジーの進化はいつも喜ばしい。問題はこれにどう向き合うかだ。

ある種の人間にとって―まあこれは僕のことなのだが―半径30m以内にいる*ように見える*人たちは、自分が到達すべき平均目標に見えてしまうことがある。こうなると不幸だ。本来は界峡トンネルを越えないと出会うことすらできなかった文字通り次元の違う存在が当然にできることを、僕はどうして努力してもなお達成できないのだろうと思い悩むことになる。立っている土台も、越えてきた屍の数も、これから対峙する課題のスケール感も違うにも関わらずだ。

結局の所、昔からお母さんに言われる「他所はよそ、家はうち」に行き着く。どこかで心のローパスフィルタで余りにも高い理想をカットしないと自分の足元もおぼつかない。インターネットは簡単に他人の人生を摂取することができ、それはアルコールのようなエンターテインメント性と常習性をもたらすが、あくまで他人の物語だ。慎ましくも愛らしい自分の人生を、諦めるのではなく、前向きに受け入れなければならない。自分の他に自分の人生の面倒を見てくれる人はいないのだから。そんなことを考えていた。

肉体と精神の不可分性

一週間ほど出張で非常に寒い地域にいて、不運にもインフルエンザに罹患してしまい寝込んでいた。ようやく帰国できたが、身体的にも精神的にも参ってしまった。

 

元々体力には自信があって、この10年ぐらいインフルはおろか風邪で休むことすら稀だったし、昨年9月で酒もやめたので「二日酔いで翌日を棒に振る」みたいな現象もまったくなくなって、健康とは完全にコントローラブルなものになっていたはずだった。

 

ところがいざ39℃の熱で数日間夢うつつの生活を送ると、健康というのがどれほど得難いものであったかという当たり前の事実を思い知らされた。とりわけ、肉体的健康とは完全に独立して存在していると思っていた精神的健康というものが、肉体的な衰弱によりあっさりと共倒れになったのは大きなショックだった。知力は体力に依存しているのだ。考えもしなかった。

 

久しぶりに出社して懸垂機にぶら下がってみると、果たして体が持ち上がらない。あれだけ毎日取り組んでいたことが、砂上の楼閣ように一夜にして失われてしまった。また、毎朝3時に起きてコツコツつづけていたアルゴリズムクイズと数学も、いまは何をやっていたのか思い出せないほどだ。特に極度に頭を使う作業が先々週ぐらいから絶不調で、一体どうなってしまったのか自分でも呆然としていたのだが、思えば病気の兆候だったのだろう。体力的にはようやく回復したが、まだ頭の中は蜘蛛の巣がかかったように茫漠としている。いまも言葉がうまく出てこない。

 

これまで、自分に対する投資はもっともROIが高いという信念をもって絶えず勉強し続けてきた。結果的に、努力はすべてを叶えないが、自分がなりたいソフトウェア技術者ぐらいはかならず努力でたどり着けるという確信を持つに至った。ところが、このような前提は肉体的な健康というものに大きく依存していることに図らずも気付かされた。これから30代後半を過ごし、40代に突入するにあたって、この基盤はますます脅かされる可能性が高い。これまでのように、ただ盲目的に自分のやれることをやってさえいればどこかにたどり着けるという確信がすこし揺らいでしまった。自分がいまの状況にあるのは、単に幸運だったのだ。慎まねばならない。

 

また近い将来、怨念にも似た執念の火が自分の中で燃え始め、再び元の自分に戻れる日がくると信じているが、それまではほんの少しだけ無茶な早起きをやめてしばらく自分の健康と向き合いたいと思う。命あっての物種。人生は長く短い。このバランスが最も難しい。それでは、近いうちにまた。

レッテル貼りを嫌っていた自分がレッテル貼りモンスターになっていた話

ここのところずっと考えていたことが自分の中でまとまってきたので言語化する。

僕の非常によくない傾向として、自虐的・自罰的な物言いをするという点がある。たとえば、

  • CS学位がないので何をやってもダメ
  • 算数がわからないので競プロやる資格なし
  • 英語サッパリできない、TOEIC900点は現地校の幼稚園なみ

というようなやつだ。これは正直に言って、自分の中では経験に基づく事実のようなもので、いずれも「だから何とかしなくては!」という部分が自分に対してかかっているのだが、言い方が稚拙すぎて、同一属性の全人類に対して銃口を向けていることに気付いていなかった。

「僕は文系なので…」みたいな自虐もそうだ。そもそも自分がこういうレッテル貼りに対して人一倍憤慨していたはずなのに。

この画像が一時期Twitterで流行っていて「文系はこんなのも解けないのか」みたいなdisりをみるたびに、微分導関数ぐらい導けるわいふざけんなよという思いがあった。"文系"を無学のエイリアスとして使うんじゃないよと。これは、ともすれば"文系"を自分に対する言い訳として使っていた軟弱な態度と完全に矛盾する。それに、理転して情報科学の大学院に進学した今では、この言葉は自虐としてすら機能していない。自分が最も嫌い、軽蔑していたレッテル貼りを自ら進んで行うモンスターになっていたということだ。いまは自分を恥じている。

US駐在したときもそうだった。日本では謎のコンサルが「シリコンバレーのエンジニアは年収2000万!」みたいな物価も何も考えてねえだろみたいなことを言うのを見て疲弊し、たまに現地就職の日本人にまで「駐在組はクビにもならないし家賃補助もあっていいですよね」と謎の憎しみを向けられたりしたものだ。どれも実態を伴わないレッテル貼りによるものだ。全部自分が嫌っていたやつだ。現代のイソップ童話の登場人物に自分がなってしまっていた。

僕個人としては次の点に気を付けたい。

  • 自分の属するカテゴリについて言及しない。常に主語は自分とする。
  • 自虐を控える。
  • 何かを貶めない。批判は建設的対案とともにする。

自分の娘に見られて恥ずかしくないことだけインターネットに書こうと思います。それではポケモン捕まえてきます。

とにかく次の10年を生き残りたい

ついに2020年になってしまった。

この10年はとにかくネイティブアプリ開発にすべてを投資した。
京都GTUGのイベント情報によれば、第1回Androidハッカソンは2009年9月5日に開催されている。このハッカソンのためにドコモ HT-03Aを契約しに行ったのがAndroidとの出会いだった。当時APIレベル3、コードネームCupcake、バージョン1.5である。加速度がホントにJavaのコードから取れてびっくりしたのを覚えている。

実に丸10年の月日が過ぎた。スマートフォンの普及率は20-30代で90%を超え、今後も低くなるということはないだろう。当然、それらスマートフォンで動作するネイティブアプリ開発の需要もしばらくは続くに相違ない。ただし、開発の前線から見える情景は少しずつ変わってきているようにも感じられる。
3-4年前だとネイティブファースト(特にiOS)が当然であり、ウェブが存在しないサービスも少なくなかったが、ウェブへの回帰も見られるようになってきた。また、FlutterによるiOS/Androidアプリ同時開発も流行しているように思う。この辺りは個人の感覚だし、東京とそれ以外、あるいは米国、欧州でもかなり違っているのだろうと想像するが、本ポエムの主眼ではない。

次の10年は何に投資すればよいのだろうか。本当に知りたい。僕は深刻にこの辺りの嗅覚が鈍い。
iPhoneがリリースされた2007年当時、僕はすでにプログラマだった。当時所属していた会社はSkypeのようなP2P通信アプリを開発しており、LinuxザウルスやSymbianOSなんかにも対応していた記憶がある。その渦中にあって、iPhoneがその後天下を取ることを想像できなかった。BlackBerryNokia端末との違いが分からなかった。当事者とは案外そんなものだ。何回目かのxR元年と、いつ来るかわからないシンギュラリティ。また同じように、10年後振り返って「ああ、なんであのときアレに投資していなかったのか…」と嘆息するのだろう。

次の10年は僕の40代を捧げる10年となる。実際に35歳を過ぎてみて、新しいことに興味を持って勉強をし続ければまだまだ全然戦えるという感覚はある。理論や研究を補うために大学院にも進学した。競プロも始めた。酒もやめた。ただまあ、抗いきれない事実として体力は落ち続けているし頭の回転も少しずつ鈍っている。圧倒的な瞬発力の戦いから、総合格闘技で最後は寝技で絞めるみたいな感じになってきている。次の投資先は慎重に決めねばならない。

一旦、2020年Q1は再びウェブフロントエンドへの投資を少し増やしてみようと思っている。2013年まではフロントエンド開発(なんと、jQueryとbackbone.js)もしていたのだが、7年も経ってしまって自分の知識はバクテリアに分解されて石油となっている。その間ウェブ技術は目覚ましい発展を遂げ、現在のJSは当時とは完全に別物だと感じる。TypeScriptで型の恩恵も受けられる。ありがたいことだ。この状況を作ってくれた人々に感謝したい。

という訳で何卒諸々ご指導ください。本年も宜しくお願いいたします。

アルゴリズムと数学的思考力

厳しい。年始早々厳しさを感じている。自分のプログラミング力にだ。伸び悩んでいる。

端的に言って、数学力のなさが自分のプログラミング能力に制限をかけている。例えばこの問題。

560. Subarray Sum Equals K
入力として与えられる配列 nums のうち、合計が k となる部分配列の個数を数え上げよ。どうも有名な問題らしいが…

まず大前提として、部分配列なので i, j の2重ループで始点・終点を定めて sum(nums[i, j]) = k になるものを数え上げれば必ず答えが得られる。最悪計算量は O(N^3) ただし i < nums.length < 20000 という制約があるので N^3 では遅すぎるから何か考えてくださいというのがスタート地点。

ここで、結果の変わらない累積和を何度も求めているので nums[i, j] = k を求めたい場合、 nums[0, j] - nums[0, i - 1] としてこれまでの計算結果を利用できることに気付けると計算量を大幅に減らせる。ただし、nums には負数が含まれているので同じ累積和が何度も出現する可能性があるので Map で出現頻度をカウントする

public int subarraySum(int[] nums, int k) {
    int j = 0;
    Map<Integer, Integer> seen = new HashMap<>();
    seen.put(0, 1);
    int counter = 0;
    for (int num : nums) {
        j += num;
        int i = j - k;
        if (seen.containsKey(i)) {
            counter += seen.get(i);
        }
        seen.put(j, seen.getOrDefault(j, 0) + 1);
    }
    return counter;
}

これで時間計算量 O(N) 空間計算量 O(N) で通った。 seen.put(0, 1) の部分だけトリッキーで、このアルゴリズムがそもそも前の計算結果を利用するものなので、初めて sum(nums[0, j]) == k になるケースをカバーする。

これってもはや算数の問題で、区間 nums[i, j] = knums[0, j] - nums[0, i - 1] で求まるっていうのは答え見て初めてマツコデラックスが感心する時の声で「ハ〜〜〜〜〜〜!」って唸ることしかできなかった。もう1題。

373. Find K Pairs with Smallest Sums
ソート済み配列 nums1, nums2 と整数 k が与えられる。このとき nums1, nums2 から1つずつ取り出してつくるペア (u,v) を、和が小さい順に k 個取り出せ。

Input: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
Output: [[1,2],[1,4],[1,6]] 

これも O(N^2) で全ペアが列挙でき、それを整列するコストが O(N log N) なので最悪計算量 O(N^2) で必ず解ける問題。これには特に nums.length, k の制約が示されていないが、AtCoderなどのコンテストでは間違いなく O(N^2) では制限時間に収まらない(=不正解となる)データが与えられるだろう。したがってもっと高速なアルゴリズムを考えなさいというのが趣旨となる。

ここで nums1, nums2 はソート済みなので、nums1[0], nums2[0] は常に最小のペアであることが保証される。このような最小のインデックスをそれぞれ i, j とした場合、nums1[i], nums2[j] が解答のひとつならば nums1[i + 1], nums2[j]nums1[i], nums2[j + 1] が次の答えの候補になる。言われてみればそうである(孫権顔)

後は候補が複数あった場合に低コストで最小値を得られるデータ構造を考える。そう、二分ヒープ(最小)である。min heap の追加・削除の最悪コストは O(log N) なので、今回要素数 k の候補から常に最小を取り出しつつ k 個集めるまで繰り返すので O(K log K) が模範解答のひとつとなる。本件は競プロ界で著名な 宇宙ツイッタラーX さんに解説いただいて初めて理解できた。スレッドを参照されたい。

実装例も、氏のそれより綺麗には到底書けないので 氏のgist を見ていただきたい。

で、ここからが主題なのだが、これらの問題はプログラミング能力というか算数の問題なのだ。最初の問題はデータ構造と言っても HashMap ぐらいしか使ってないし、2題目も min heap こそ確かにコンピュータ・サイエンス的なデータ構造ではあるものの、これは最小(最大)値を O(log N) で出し入れできる(最小(最大)値の参照だけなら O(1))データ構造とだけ覚えていれば使える類のものだし、そこがこの問題を解く最大のキーというわけではない。現に僕は min heap を使うと模範解答になるというヒントを貰った後も「 nums1[i], nums2[j] が答えならば nums1[i + 1], nums2[j]nums1[i], nums2[j + 1] がその候補になる」という核心部分が出てこなかったし、むしろここがこの問題の最大のキーポイントであるわけで。

ここからは自分でもうまく整理できていないのだが、これは「数学力」なんだろうか。僕が数学が苦手なのは自他ともに認めるところだが、いま大学院の授業についていくためにマセマの微分積分とかを解いてるけど、なんというかこう、公式あてはめて微分するとかとは違うわけじゃないですか。使われているのは中学生レベルの四則演算で、むしろ「数学的思考力」を問われているというか。

社会人になってプログラマとして10年ぐらいやっていて忘れかけていたけど、そういえば僕は勉強まったくできなかったな、受験敗者だったわ、忘れてた。僕には数学的思考力が致命的にない。こういうのをアルゴリズムクイズを解いていると嫌というほど思い出させられる。

今回の2題は非常に悔しい思いをしたので覚えたが、これを繰り返していけば僕のように頭がよくない人間も類題を解けるようになるのか、それともちょっとひねられるだけでまたまったくお手上げになってしまって悔し涙に枕を濡らすのか、いまは分からない。願わくは2年後に読み返したときに「あの時はあんなしょうもない問題で悩んでいたんだなぁ😅」と思えるようになっていたいが、どうやることやら。正月に書くような内容ではないな…

Amplify for Androidでより直感的になったGraphQLを試した

TL;DR

本エントリはAWS Amplify Advent Calendar 2019の24日目です!

今回は新しくリリースされたAmplify for Androidを使って、前回と同じく下のようなチャットアプリを作るので、興味のある方は前口上を飛ばして後半をお読みください🎄

f:id:fushiroyama:20191201023816g:plain

Amplifyとは

本題に入る前に、Amplifyを最近よく耳にするようになったけど何かよく分かっていないという人向けの説明をします。

Amplifyはモバイルバックエンドを爆速で作るためのサービスです。 https://aws.amazon.com/jp/amplify/

他のいわゆるモバイルバックエンドとの際立った違いのうち、個人的に強調したいのは以下2点です。

  • バックエンドはAWSのサービスであり、真の意味でスケールする
  • GraphQLのマネージドサービスを利用できる本日時点で唯一のプラットフォームである

Amplifyの良い点として、フロントエンドから利用する時はそれと意識せずに簡単に構築できるけれども、バックエンドはAWSが個別に提供するサービスで構成されているという点です。これは、バックエンドをキャパシティのよく分からないブラックボックスとして扱うことなく、必要であればAWSを使い慣れたインフラ部隊と協力して本当の意味でスケールするようにチューニング可能だということです。

さらに、AmplifyはGraphQLのマネージドサービスを提供する今日時点で唯一のプラットフォームです。GraphQLの良さは他の記事に譲りますが、これを自前で構築するのは中々骨が折れます。AmplifyでAPIを追加するときにGraphQLを選択すると、裏ではAWS AppSyncが使われますが、これはGraphQLのマネージドサービスであり、サーバのプロビジョニングやチューニングを気にすることなくGraphQLのメリットを享受できます。

Amplifyの構成要素

Amplifyは主に次の要素で構成されています

  • バックエンドプロビジョニングのためのCLI
  • ウェブ、iOS/Androidに組み込むためのフレームワーク
  • CI/CDや管理のためのコンソール

f:id:fushiroyama:20191229142819p:plain
AWS re:Invent 2019のセッション動画より引用

後述しますがAmplify for iOS/AndroidのリリースでXcode/Android Studioとの統合がすすみ、CLIを単独で使用することは今後かなり少なくなります。したがって、基本的には普段開発しているときのようにCocoaPodsやGradleでライブラリを導入するように使い始めることができます。具体的な使い方はこのエントリの後半で解説します。

Amplifyのカテゴリ

Amplifyはカテゴリという概念があり、

  • APIREST APIやGraphQL
  • Auth…認証と認可
  • Storage…ストレージ
  • Analytics…分析とユーザエンゲージメント
  • Predictions…AI/MLの組み込み
  • XR…AR/VR

のような機能のうち、使いたいものを好きなだけ選んで使うことができます。

これまでもモバイルからAWSのサービスを使うことは当然できました。これはAWS Mobile SDKによって簡単に実現できます。 ただ、これはどちらかと言えば「AWSのこのサービスを組み込みたい」というマインドセットで使います。対してAmplify for iOS/Androidでは「APIを組み込みたい」というように、ユースケースの側面から使うことができます。

f:id:fushiroyama:20191229144655p:plain
AWS re:Invent 2019のセッション動画より引用

前置きが長くなりました。ここまでがAmplifyの概要です。次からはAmplify for iOS/Androidについて解説します。

Amplify for iOS/Androidとは

先日のAWS re:Invent 2019で新しく発表された、Amplifyをモバイルからより簡単に使うためのアップデートです(公式ブログ)。

fushiroyama.hatenablog.com

先日、上記エントリで AWSAppSyncClient を使ってGraphQLにつなぐ方法を紹介しましたが、これが新しいAPIでどんな感じになるのか見てみましょう。

注意点

公式サイトに明示されていますが、Amplify for iOS/Androidはプレビューリリースで、本番環境での利用はまだ想定されていません。他の多くのOSSがそうであるように、開発者が想定していないと思われることをやろうとするとまだ粗削りな部分が見て取れますが、コントリビューションのチャンスでもあります!本エントリで興味を持ってくださった方は是非お試しください!

Androidクイックスタート

それではAmplify for Androidを使ってGraphQLの読み書きをしてみたいと思います。序盤は公式サイトの手順に準じます。

まずはプロジェクトの build.gradleプラグインをインストールします。

buildscript {
    ext.kotlin_version = '1.3.61'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // Amplify
        classpath 'com.amplifyframework:amplify-tools-gradle-plugin:0.1.0'
    }
}

// Apply Plugin
apply plugin: 'com.amplifyframework.amplifytools'

allprojects {
    repositories {
        google()
        jcenter()

    }
}

次にアプリケーションレベルの build.gradle にJava8の設定と依存関係のインストールをします。

android {
  compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}

dependencies {
  implementation 'com.amplifyframework:core:0.9.0'
  implementation 'com.amplifyframework:aws-api:0.9.0'
}

次に、Android StudioをProjectビューにして amplify/backend/api/amplifyDatasource/schema.graphql を編集します。これはGraphQLのスキーマ定義で、これを元にAndroid用のモデルが自動生成されます。せっかくなので、以前のエントリと同じくリアルタイムチャットアプリを開発してみます。まずは、前回と同じ次の定義で作ってみましょう。

type Message @model {
  id: ID!
  username: String!
  content: String!
}

次に、Amplify CLIをインストールします。npmを使うのでNode.jsが入っていない人はインストールしてください。

$ npm install -g @aws-amplify/cli
$ amplify -v
4.7.0

初めてAmplifyを利用する場合は、CLIAWSのクレデンシャルを設定する必要があるので amplify configure コマンドを実行します。AWSのマネジメントコンソールにリダイレクトされるので、そこでユーザを作成してキーを発行してください。ここは次のチュートリアルに非常に詳細な解説があります。

この状態でAndroid Studioに戻ってくると、Build > Make Project でプロジェクトをビルドします。

f:id:fushiroyama:20191229153500p:plain

すると、Graldeメニューに modelgenamplifyPush のタスクが追加されます。

f:id:fushiroyama:20191229153610p:plain

まず、modelgen を実行すると先程のGraphQLのスキーマから com.amplifyframework.datastore.generated.model にメッセージを表すモデルの Message.java が生成されます。 次に amplifyPush を実行すると、Amplify CLIが必要なバックエンドをプロビジョニングしてくれます。この作業は数分かかることがあります。 完了したら、これらを使ってチャットのメッセージを読み書きしてみましょう。

最初にAmplifyを初期化するための設定をします。カスタムApplicationクラスを作成し、次の内容を追記します。

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        try {
            Amplify.addPlugin(AWSApiPlugin())
            Amplify.configure(applicationContext)
        } catch (e: AmplifyException) {
            Log.e(TAG, e.message)
            throw RuntimeException(e)
        }
    }
}

MyApplicationAndroidManifest.xml に追加するのを忘れないでください。

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

あとは読み書きをするだけです!まずはミューテーションから。

modelgen によって作られたモデルは Message.builder() のようなビルダーがついてきます。ユーザの入力に合わせて簡単にモデルを組み立てることができます。 あとは Amplify.API.mutate メソッドを呼ぶだけです。結果を受け取りたい場合は ResultListener も一緒に渡します。

private fun mutation(
    username: String,
    content: String
) {
    val message: Message =
        Message.builder()
            .username(username)
            .content(content)
            .build()

    Amplify.API.mutate<Message>(
        message,
        MutationType.CREATE,
        object :
            ResultListener<GraphQLResponse<Message>> {
            override fun onResult(response: GraphQLResponse<Message>) {
                response.data?.let {
                  // do things with message
                }
            }

            override fun onError(e: Throwable) {
            }
        })
}

次はクエリです。こちらも Amplify.API.query メソッドを呼ぶだけです。 結果は同じように ResultListener で受け取れるので、これを RecyclerView で描画するといった使い方が一般的でしょう。

private fun query() {
    Amplify.API.query(
        Message::class.java,
        object :
            ResultListener<GraphQLResponse<Iterable<Message>>> {
            override fun onResult(response: GraphQLResponse<Iterable<Message>>) {
                val messages: MutableList<Message> = mutableListOf()
                for (message in response.data) {
                    messages.add(message)
                }

                // update RecyclerView
                updateMessages(messages)
            }

            override fun onError(e: Throwable) {
            }
        })
}

最後にサブスクリプションです。なんとこちらも Amplify.API.subscribe メソッドを呼ぶだけです。 これだけでメッセージの作成をリアルタイムに通知してもらえるので、チャットアプリのようなものがいとも簡単に作成できます。

private fun subscribe() {
    Amplify.API.subscribe(
        Message::class.java,
        SubscriptionType.ON_CREATE,
        object :
            StreamListener<GraphQLResponse<Message>> {
            override fun onNext(response: GraphQLResponse<Message>) {
                response?.data?.let {
                    // update RecyclerView
                    addMessage(it)
                }
            }

            override fun onComplete() {
            }

            override fun onError(e: Throwable) {
            }
        }
    )
}

めちゃくちゃ簡単ですね!ここまでが公式のチュートリアルに相当する内容です。

時系列でメッセージを取得

ここからは少し発展的な内容を扱います。

AmplifyでGraphQLを使う場合、デフォルトではAppSyncとDynamoDBが使われます。DynamoDBは扱うデータサイズや規模によらず、うまく設計すればミリ秒単位のパフォーマンスを維持できるKVSおよびドキュメントデータベースですが、一般的なRDBMSを1台だけで扱うときとは違う点を考慮する必要がある場合があります。たとえば「メッセージを投稿順で取得し、ページングさせたい」という場合には、DynamoDBではいくつか実現方法がありますが、

として設計するのがよくあるパターンです。ここではDynamoDBの詳細なデザインパターンには触れませんが、チャットサービスといえば普通はチャンネル(部屋)を複数持てるのが普通なので、各部屋を表現する roomId + メッセージの日付 createdAt を使ってメッセージを時系列に取得することにします。

まずは前出のGraphQLのスキーマを次のように変更します。

type Message
  @model
  @key(name: "SortByCreatedAt", fields:["roomId", "createdAt"], queryField: "listMessagesSortedByCreatedAt" )
{
  id: ID!
  username: String!
  content: String!
  roomId: String!
  createdAt: AWSDateTime!
  updatedAt: AWSDateTime
}

name: "SortByCreatedAt", fields:["roomId", "createdAt"] の部分で時系列に取得するためのキーを追加しています。 また queryField: "listMessagesSortedByCreatedAt" の部分で、このキーを使ったクエリをする際の名前を指定しています。

できたら、以前同様 modelgenamplifyPush を実行してモデルファイルを再生成し、バックエンドにプロビジョニングします。 これで、GraphQL的には次のようなクエリで時系列で取得できるはずです。

query GetPost(
  $roomId: String!
  $limit: Int!
) {
  listMessagesSortedByCreatedAt(
    roomId: $roomId
    sortDirection: ASC
    limit: $limit
  ) {
    items {
      id
      content
      username
      roomId
      createdAt
    }
  }
}

ただし、現状のAmplify for Androidではこのような任意のクエリを簡単に送る方法がないので、GraphQLRequest を利用します。 GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer()) のように使います。

注意点として、スキーマcreatedAt: AWSDateTime! のように定義した場合、自動生成された Message クラスでは日付は java.util.Date 型として生成されますが、このモデルを透過的にGraphQLのクエリに変更してくれるライブラリ組み込みの GsonVariablesSerializerjava.util.Date を雑に yyyy-MM-dd に変換します。 これでは精度的に問題なので、ISO 8601 にするために独自の GsonVariablesSerializer を作成します。

class GsonVariablesSerializer : VariablesSerializer {
    override fun serialize(variables: Map<String, Any>): String {
        return GsonBuilder()
            .registerTypeAdapter(
                Date::class.java,
                DateSerializer()
            )
            .create()
            .toJson(variables)
    }

    internal inner class DateSerializer : JsonSerializer<Date?> {
        override fun serialize(
            date: Date?,
            typeOfSrc: Type,
            context: JsonSerializationContext
        ): JsonElement {
            val df: DateFormat = SimpleDateFormat(
                "yyyy-MM-dd'T'HH:mm:ssXXX",
                Locale.getDefault()
            )
            return JsonPrimitive(df.format(Date()))
        }
    }
}

あとは次のようにクエリを組み立てて、GraphQLRequest を作成して Amplify.API.query します。 残念ながらKotlinのraw stringsでは$自体の文字として扱えないので${'$'}のようにしています。

val query = """
    query GetPost(
      ${'$'}roomId: String!
      ${'$'}limit: Int!
    ) {
      listMessagesSortedByCreatedAt(
        roomId: ${'$'}roomId
        sortDirection: ASC
        limit: ${'$'}limit
      ) {
        items {
          id
          content
          username
          roomId
          createdAt
        }
      }
    }
    """.trimIndent()
val variables: Map<String, Any> = mutableMapOf(
    "roomId" to DEFAULT_ROOM,
    "limit" to DEFAULT_LIMIT
)
val request =
    GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer())

val listener = object :
    ResultListener<GraphQLResponse<Iterable<Message>>> {
    override fun onResult(response: GraphQLResponse<Iterable<Message>>) {
        val messages: List<Message> = response?.data?.map { it }.orEmpty()
        // do things with messages
    }

    override fun onError(e: Throwable) {
    }
}
Amplify.API.query(request, listener)

ミューテーションもまったく同様です。

val query = """
    mutation CreateMessage(
      ${'$'}id: ID!
      ${'$'}content: String!
      ${'$'}roomId: String!
      ${'$'}username: String!
      ${'$'}createdAt: AWSDateTime!
    ) {
      createMessage(input: {
        id: ${'$'}id
        content: ${'$'}content
        roomId: ${'$'}roomId
        username: ${'$'}username
        createdAt: ${'$'}createdAt
      }) {
        id
        content
        roomId
        username
        createdAt
      }
    }
    """.trimIndent()
val variables: Map<String, Any> = mutableMapOf(
    "id" to id,
    "content" to content,
    "roomId" to roomId,
    "username" to username,
    "createdAt" to createdAt
)
val request =
    GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer())

Amplify.API.mutate(
    request,
    object :
        ResultListener<GraphQLResponse<Message>> {
        override fun onResult(response: GraphQLResponse<Message>) {
        }

        override fun onError(e: Throwable) {
        }
    }
)

これで、時系列にメッセージを読み書きするための対応ができました。

改善を望む内容

最後に今回気付いたいくつかの今後改善されるであろう内容を取り上げます。

まず、Amplify.API.query, Amplify.API. mutate, Amplify.API.subscribe はいずれも現在のところブロッキングAPIとなっています。これは、LoaderObservable 等でラップして扱うことを意図しているのか、それとも単に設計上のミスなのか判然としません。とにかく、このままではUIをブロックしてしまうので、RxJavaの Single, Completable でラップして使いました。

// query
val source: SingleOnSubscribe<List<Message>> = SingleOnSubscribe {
    val listener = object :
        ResultListener<GraphQLResponse<Iterable<Message>>> {
        override fun onResult(response: GraphQLResponse<Iterable<Message>>) {
            val messages: List<Message> = response?.data?.map { it }.orEmpty()
            it.onSuccess(messages)
        }

        override fun onError(e: Throwable) {
        }
    }
    Amplify.API.query(request, listener)
}
return Single.create(source)

// mutation
return Completable.create { emitter ->
    Amplify.API.mutate(
        request,
        object :
            ResultListener<GraphQLResponse<Message>> {
            override fun onResult(response: GraphQLResponse<Message>) {
                response.data?.let {
                    emitter.onComplete()
                }
            }

            override fun onError(e: Throwable) {
                emitter.onError(e)
            }
        })
}

// subscription
return Single.create { emitter ->
    Amplify.API.subscribe(
        Message::class.java,
        SubscriptionType.ON_CREATE,
        object :
            StreamListener<GraphQLResponse<Message>> {
            override fun onNext(response: GraphQLResponse<Message>) {
                response?.data?.let {
                    emitter.onSuccess(it)
                }
            }

            override fun onComplete() {
            }

            override fun onError(e: Throwable) {
                emitter.onError(e)
            }
        }
    )
}

利用する側は次の通り subscribeOn(Schedulers.io()) とすればワーカスレッドで処理が実行されます。

compositeDisposable.add(
    mutation(
        name,
        editTextContent.text.toString(),
        DEFAULT_ROOM
    )
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribeBy(
            onComplete = {
                editTextContent.setText("")
            },
            onError = {
                Log.e(TAG, it.message, it)
            }
        )
)

APIとしてListenerを受け取る以上、設計としては非同期なコールバックとして意図されている気がします。この辺りは折角のオープンソースなので何らかのフィードバックを行いたいと思います。

それから、前回のエントリ同様に、AWSMobileClient を使うことで認証機能を簡単に追加することができますが、認証にAWSのCognito User Poolsを使った場合Subscriptionがうまく動かないようです。SubscriptionAuthorizationHeader.from の実装を見る限り、まだAPI Keyにしか対応していないのかも知れません。この辺り、やはりまだまだプロダクション・レディとは言えません。

まとめ

ということで、駆け足になりましたが、今月プレビューリリースされたばかりのAmplify for Androidをざっと触ってみた感じをご紹介しました。 ライブラリとしてはまだまだこれから成熟していくのに期待しますが、AmplifyはたとえばAppSyncのコンソールで非常に簡単にクエリを実行して確認できるなど、デベロッパー体験が非常によいです。Amplifyは正直もっと流行っていいと思っています。これからも情報発信してゆくのでどうかお楽しみに。

それではよいお年を。