TL;DR
本エントリはAWS Amplify Advent Calendar 2019の24日目です!
今回は新しくリリースされたAmplify for Androidを使って、前回と同じく下のようなチャットアプリを作るので、興味のある方は前口上を飛ばして後半をお読みください🎄
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は主に次の要素で構成されています
後述しますがAmplify for iOS/AndroidのリリースでXcode/Android Studioとの統合がすすみ、CLIを単独で使用することは今後かなり少なくなります。したがって、基本的には普段開発しているときのようにCocoaPodsやGradleでライブラリを導入するように使い始めることができます。具体的な使い方はこのエントリの後半で解説します。
Amplifyのカテゴリ
Amplifyはカテゴリという概念があり、
- API…REST APIやGraphQL
- Auth…認証と認可
- Storage…ストレージ
- Analytics…分析とユーザエンゲージメント
- Predictions…AI/MLの組み込み
- XR…AR/VR
のような機能のうち、使いたいものを好きなだけ選んで使うことができます。
これまでもモバイルからAWSのサービスを使うことは当然できました。これはAWS Mobile SDKによって簡単に実現できます。 ただ、これはどちらかと言えば「AWSのこのサービスを組み込みたい」というマインドセットで使います。対してAmplify for iOS/Androidでは「APIを組み込みたい」というように、ユースケースの側面から使うことができます。
前置きが長くなりました。ここまでがAmplifyの概要です。次からはAmplify for iOS/Androidについて解説します。
Amplify for iOS/Androidとは
先日のAWS re:Invent 2019で新しく発表された、Amplifyをモバイルからより簡単に使うためのアップデートです(公式ブログ)。
先日、上記エントリで 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を利用する場合は、CLIにAWSのクレデンシャルを設定する必要があるので amplify configure
コマンドを実行します。AWSのマネジメントコンソールにリダイレクトされるので、そこでユーザを作成してキーを発行してください。ここは次のチュートリアルに非常に詳細な解説があります。
この状態でAndroid Studioに戻ってくると、Build > Make Project
でプロジェクトをビルドします。
すると、Graldeメニューに modelgen
と amplifyPush
のタスクが追加されます。
まず、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) } } }
MyApplication
は AndroidManifest.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 { Log.i(TAG, it.toString()) runOnUiThread { editTextContent.setText("") } } } override fun onError(e: Throwable) { e.message?.let { toast(it) Log.e(TAG, it, e) } } }) }
次はクエリです。こちらも 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) { e.message?.let { toast(it) Log.e(TAG, it, e) } } }) }
最後にサブスクリプションです。なんとこちらも 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() { Log.i(TAG, "subscription onCompleted") } override fun onError(e: Throwable) { e.message?.let { toast(it) Log.e(TAG, it, e) } } } ) }
めちゃくちゃ簡単ですね!ここまでが公式のチュートリアルに相当する内容です。
時系列でメッセージを取得
ここからは少し発展的な内容を扱います。
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"
の部分で、このキーを使ったクエリをする際の名前を指定しています。
できたら、以前同様 modelgen
と amplifyPush
を実行してモデルファイルを再生成し、バックエンドにプロビジョニングします。
これで、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のクエリに変更してくれるライブラリ組み込みの GsonVariablesSerializer
は java.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) { e.message?.let { } } } 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となっています。これは、Loader
や Observable
等でラップして扱うことを意図しているのか、それとも単に設計上のミスなのか判然としません。とにかく、このままでは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() val text = messages.joinToString() Log.i(TAG, text) it.onSuccess(messages) } override fun onError(e: Throwable) { e.message?.let { toast(it) Log.e(TAG, it, e) } } } 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 { Log.i(TAG, it.toString()) emitter.onComplete() } } override fun onError(e: Throwable) { e.message?.let { Log.e(TAG, it, e) } 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 { Log.i(TAG, "subscription onCompleted") emitter.onSuccess(it) } } override fun onComplete() { Log.i(TAG, "subscription onCompleted") } override fun onError(e: Throwable) { e.message?.let { Log.e(TAG, it, e) } 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は正直もっと流行っていいと思っています。これからも情報発信してゆくのでどうかお楽しみに。
それではよいお年を。