こんにちは!本エントリは「AWS Amplify Advent Calendar 2019」の記念すべき第1日目です!
このエントリではAWS Amplifyを使ってユーザ認証機能付きのリアルタイムチャットアプリを30分で作ってみたいと思います。
AWS Amplifyとは
AWS Amplify(以下Amplify)を初めて耳にする方、ご安心ください!
Amplifyはスケールするモバイルアプリおよびウェブアプリを最速で構築するためのサービス/ツール群です。
Amplifyは
などを含んでおり、モバイルアプリやウェブアプリを開発して実際にサービスを提供するのに必要なフロントエンド/バックエンドを非常に簡単に用意することができます。実際にその様子をお見せしましょう!
今回作るアプリ
冒頭で述べたように、ユーザ認証機能付きのリアルタイムチャットアプリを作ってみたいと思います。 このアプリは次のような機能を備えています。
- アプリから簡単にユーザ登録してログインできる
- ログインしたらこれまでのチャット履歴が表示される
- 好きな文章を入力して送信できる
- 任意の人数がチャットに参加でき、参加者のコメントは能動的にリロードする必要なくリアルタイムに表示される
- ログアウトしてチャットを終了できる
APIにGraphQLを使ってみる
AmplifyでAPIを追加するとき、主に2つの選択肢があります。
RESTful APIについては言わずもがな。これは裏ではAmazon API GatewayとAWS Lambdaによって実現されますが、こちらは想像がつきやすいので今回は利用しません。
今回はRESTにかわる規格として注目を集めているGraphQLを利用してみたいと思います。GraphQLに関しては「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶが素晴らしい記事ですので別途ご覧ください。
AWSではAWS AppSyncというGraphQLのマネージドサービスが提供されています。マネージドサービスということは、自分たちでサーバを用意しなくても簡単にGraphQLを利用できるということです!
AmplifyでGraphQLをAPIに選択すると裏ではこのAppSyncが使われますが、今回のアプリを作るにあたってはバックエンドで何が使われているかといった内容はほとんど意識することなく簡単に開発することができます。
Androidプロジェクトを作成
今回はAndroidアプリでGraphQLを使ってみたいと思います。本エントリのiOS対応版も近い内に公開予定なので楽しみにしてください!☺️
AndroidアプリはAndroid Studioのウィザードから普通に作成して、アプリが起動するのを確認できればOKです。ここを便宜上 ${PROJECT_ROOT}
とします。
Amplify CLIのインストール
Amplify CLIというコマンドラインツールをインストールして、これで対話的に機能を追加したり編集するのがAmplifyを利用する基本的な流れとなります。
ターミナルから次のコマンドでAmplify CLIをインストールします。環境によっては sudo
が必要です。
$ npm install -g @aws-amplify/cli $ amplify configure
amplify configure
でAWSにIAMユーザを作成し、そのユーザ権限でCLIを実行できるようになります。 この辺りに少し不慣れな方はAWS Amplify ハンズオン 基本ステップに画像つきで詳細に解説されているので参考にしてください!
amplify init
次に ${PROJECT_ROOT}
で amplify init
コマンドを実行し、AndroidプロジェクトにAmplifyをセットアップします。
- Enter a name for the project
AmplifyAndroid
- Enter a name for the environment
dev
- Where is your Res directory:
app/src/main/res
あたりを選択/入力すれば、あとはデフォルトで問題ありません。
$ amplify init Note: It is recommended to run this command from the root of your app directory ? Enter a name for the project AmplifyAndroid ? Enter a name for the environment dev ? Choose your default editor: Vim (via Terminal, Mac OS only) ? Choose the type of app that you're building android Please tell us about your project ? Where is your Res directory: app/src/main/res Using default provider awscloudformation For more information on AWS Profiles, see: https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html ? Do you want to use an AWS profile? Yes ? Please choose the profile you want to use default ⠋ Initializing project in the cloud... ✔ Successfully created initial AWS cloud resources for deployments. ✔ Initialized provider successfully. Initialized your environment successfully. Your project has been successfully initialized and connected to the cloud! Some next steps: "amplify status" will show you what you've added already and if it's locally configured or deployed "amplify <category> add" will allow you to add features like user login or a backend API "amplify push" will build all your local backend resources and provision it in the cloud "amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud Pro tip: Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything
ここで何が行われているかは、AWS Amplify ハンズオン 基本ステップの「このとき何が起きているか」を参照してください。
無事に完了すると amplify status
でこのプロジェクトのAmplifyの設定を見ることができます。
$ amplify status | Category | Resource name | Operation | Provider plugin | | -------- | ------------- | --------- | --------------- |
いまは何もしていないのでこれで問題ありません。
GraphQL(AppSync)セットアップ
このプロジェクトにGraphQLをセットアップします!
amplify add api
で「GraphQL」を選択します。 本アプリでは認証機能を利用するので、認証は API Key
ではなく API Amazon Cognito User Pool
を選択します。
$ amplify add api ? Please select from one of the below mentioned services GraphQL ? Provide API name: amplifyandroid ? Choose an authorization type for the API Amazon Cognito User Pool Using service: Cognito, provided by: awscloudformation The current configured provider is Amazon Cognito. Do you want to use the default authentication and security configuration? Default configuration Warning: you will not be able to edit these selections. How do you want users to be able to sign in when using your Cognito User Pool? Email Warning: you will not be able to edit these selections. What attributes are required for signing up? (Press <space> to select, <a> to toggle all, <i> to invert selection)Email Successfully added auth resource
次にGraphQLのスキーマを変更します。今回はチャットのメッセージとして
- 一意なID
- メッセージの送信者
- メッセージ本文
を持ったMessageというモデルを作ることにします。
次の例のように選択して進んでいき、 ${PROJECT_ROOT}/amplify/backend/api/amplifyandroid/schema.graphql
を編集します。
? Do you have an annotated GraphQL schema? No ? Do you want a guided schema creation? Yes ? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, description) ? Do you want to edit the schema now? Yes Please edit the file in your editor: /Users/shiroyaf/git/amplify/AmplifyAndroid/amplify/backend/api/amplifyandroid/schema.graphql ? Press enter to continue
モデルは次のようにします。
type Message @model { id: ID! username: String! content: String! }
編集したらウィザードを進めます。
GraphQL schema compiled successfully. Edit your schema at /Users/shiroyaf/git/amplify/AmplifyAndroid/amplify/backend/api/amplifyandroid/schema.graphql or place .graphql files in a directory at /Users/shiroyaf/git/amplify/AmplifyAndroid/amplify/backend/api/amplifyandroid/schema Successfully added resource amplifyandroid locally Some next steps: "amplify push" will build all your local backend resources and provision it in the cloud "amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud
ここまで来たらセットアップ完了です!
amplify status
するとローカルに「Auth機能」と「Api機能」が追加されています。
| Category | Resource name | Operation | Provider plugin | | -------- | --------------- | --------- | ----------------- | | Auth | xxxxxxxxxxxxxxx | Create | awscloudformation | | Api | amplifyandroid | Create | awscloudformation |
amplify push
することで、このバックエンド構成がそのままAWS上にプロビジョニングされます。
とっつきにくいのはここまでです!ここからは楽しい楽しいアプリ開発です!
Androidアプリへの組み込み
このあと慎重な人であれば、AWSマネジメントコンソールから認証機能の実態である Cognito User Pool
にユーザを作成してログインを試みたり、GraphQLのコンソールからクエリを発行したりしたいところですが、今回は敢えてそのあたりに可能な限り触れません。その辺りを意識しなくても開発できるのがAmplifyの良さだと思うからです。
なので、いきなりアプリを書き始めます。 まずGetting Startedを参考に必要なライブラリや設定を追加します。 どれも、Androidエンジニアにはお馴染みの設定なので特に詰まることもないでしょう。
// project's build.gradle classpath 'com.amazonaws:aws-android-sdk-appsync-gradle-plugin:2.9.+'
// app's build.gradle apply plugin: 'com.amazonaws.appsync' dependencies { //AWS Base SDK implementation 'com.amazonaws:aws-android-sdk-core:2.15.+' //AppSync SDK implementation 'com.amazonaws:aws-android-sdk-appsync:2.8.+' implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.0' implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1' // Needed for AppSync Subscription https://github.com/eclipse/paho.mqtt.android/issues/321 implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' // Cognito implementation 'com.amazonaws:aws-android-sdk-cognitoidentityprovider:2.9.+' //For AWSMobileClient only: implementation 'com.amazonaws:aws-android-sdk-mobile-client:2.15.+' //For the drop-in UI also: implementation 'com.amazonaws:aws-android-sdk-auth-userpools:2.15.+' implementation 'com.amazonaws:aws-android-sdk-auth-ui:2.15.+'' }
// AndroidManifest.xml <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application 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"> <service android:name="org.eclipse.paho.android.service.MqttService" /> </application>
認証機能の実前
前出の手順で amplify init
を正しくセットアップできていると、 ./app/src/main/res/raw/awsconfiguration.json
というファイルが作成されているはずです。これはAndroidアプリからAmplifyを利用するための設定を中央集権的にするための設定ファイルです。
念の為中を確認し、CognitoUserPool
など設定がきちんとされているか確認してください。PoolId
や AppClientId
等はAWSマネジメントコンソールの Cognito User Pool
の設定画面で見つけることができます。
$ cat ./app/src/main/res/raw/awsconfiguration.json { "UserAgent": "aws-amplify-cli/0.1.0", "Version": "1.0", "IdentityManager": { "Default": {} }, "CognitoUserPool": { "Default": { "PoolId": "ap-northeast-1_xxxxxxxxx", "AppClientId": "xxxxxxxxxxxxxxxxxxxxxxxxxx", "AppClientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "Region": "ap-northeast-1" } }, "AppSync": { "Default": { "ApiUrl": "https://xxxxxxxxxxxxxxxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql", "Region": "ap-northeast-1", "AuthMode": "AMAZON_COGNITO_USER_POOLS", "ClientDatabasePrefix": "amplifyandroid-dev_AMAZON_COGNITO_USER_POOLS" } } }
いよいよAndroidのコードに取り掛かります。 まずはGraphQLのクエリ(取得)やミューテーション(追加/変更)などの操作のすべての起点となる AWSAppSyncClient
を作ります。
次の例を参考に、ビルダーには cognitoUserPoolsAuthProvider
を指定するのを忘れないようにします。
lateinit var awsAppSyncClient: AWSAppSyncClient override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val awsConfig = AWSConfiguration(applicationContext) val cognitoUserPool = CognitoUserPool(applicationContext, awsConfig) val basicCognitoUserPoolsAuthProvider = BasicCognitoUserPoolsAuthProvider(cognitoUserPool) awsAppSyncClient = AWSAppSyncClient.builder() .context(applicationContext) .awsConfiguration(awsConfig) .cognitoUserPoolsAuthProvider(basicCognitoUserPoolsAuthProvider) .build() }
次に、AWSのモバイルSDKでJWTトークンやAWSクレデンシャルを扱うための AWSMobileClient
を初期化します。
AWSMobileClient.getInstance() .initialize(applicationContext, object : Callback<UserStateDetails> { override fun onResult(userStateDetails: UserStateDetails) { Log.i(TAG, "onResult: " + userStateDetails.userState) } override fun onError(e: Exception) { Log.e(TAG, "INIT: Initialization error.", e) } })
次に、認証画面を表示する部分を作ります。
先にインストールしたdependenciesのおかげで、認証用の画面は自分で作らなくても利用することができます。呼び出しは AWSMobileClient.getInstance().showSignIn(context)
だけです。
問題は呼び出す場所ですが、AWSMobileClient
には認証状態に合わせてコールバックしてもらえるリスナが用意されているので次のように登録します。
あとは、ユーザがサインインしていない状況で showSignIn
メソッドを呼んでやるだけです。
リスナは、ログインの前後でActivityを行ったり来たりしても多重登録されないように onStart/onStop
辺りで登録解除してやるとよいでしょう。
private val userStateListener = UserStateListener { details -> Log.i(TAG, "onUserStateChanged: " + details.userState) when (details.userState) { UserState.SIGNED_IN -> { Log.i(TAG, "userState: SIGNED_IN") } else -> { Log.i(TAG, "userState: else: " + details.userState) AWSMobileClient.getInstance().showSignIn(this@MainActivity) } } } override fun onStart() { super.onStart() AWSMobileClient.getInstance().addUserStateListener(userStateListener) query() } override fun onStop() { AWSMobileClient.getInstance().removeUserStateListener(userStateListener) super.onStop() }
これでログイン機能は実装できました!
サインイン画面にはユーザの新規作成、確認コードの送信なども最初から組み込まれており完全に動作します。ここで早速ユーザを作成してログインを試してみてください。
これでログイン状態でGraphQLのAPIを自由にアクセスすることができるようになりました。
ミューテーションを実装する
GraphQLではデータの作成や変更、削除などをミューテーションと呼びます。
先にウィザードでMessageモデルを作ったことで、Amplifyが自動的にそのモデルを使って行うミューテーションのためのデータ型などを自動生成してくれます。なのでアプリ作者は
CreateMessageMutation
オブジェクトを作るAwsAppSyncClient#mutate
に対してエンキューする
だけでミューテーションを簡単に行うことができます。
次のようなメソッドを作って、ボタンクリックなどをトリガとして実行することで簡単にデータをGraphQL越しに永続化することができます。
private fun mutation( id: String = System.currentTimeMillis().toString(), username: String, content: String ) { val callback: GraphQLCall.Callback<CreateMessageMutation.Data> = object : GraphQLCall.Callback<CreateMessageMutation.Data>() { override fun onFailure(e: ApolloException) { e.message?.let { toast(it) Log.e(TAG, it, e) } } override fun onResponse(response: Response<CreateMessageMutation.Data>) { response.data()?.createMessage()?.let { Log.i(TAG, it.toString()) editTextContent.setText("") } } } val input = CreateMessageInput .builder() .id(id) .username(username) .content(content) .build() awsAppSyncClient .mutate(CreateMessageMutation.builder().input(input).build()) .enqueue(callback) }
Amplifyのウィザードでセットアップした場合、デフォルトでAmazon DynamoDBをデータソースとして扱います。したがってミューテーション後はマネジメントコンソールからデータが格納された様子を確認することができます。
クエリを実装する
同様に、GraphQLではデータ取得操作をクエリと呼びます。
クエリもまったく同じように
- 型安全にリクエストやコールバックを作成し、
- AwsAppSyncClient#query
するだけでデータを取得することができます。
private fun query() { val callback: GraphQLCall.Callback<ListMessagesQuery.Data> = object : GraphQLCall.Callback<ListMessagesQuery.Data>() { override fun onFailure(e: ApolloException) { e.message?.let { toast(it) Log.e(TAG, it, e) } } override fun onResponse(response: Response<ListMessagesQuery.Data>) { val messages: MutableList<ListMessagesQuery.Item> = response.data()?.listMessages()?.items() ?: mutableListOf() updateMessages(messages) val text = messages.joinToString() Log.i(TAG, text) } } awsAppSyncClient.query( ListMessagesQuery .builder() .limit(DEFAULT_LIMIT) .build() ) .responseFetcher(AppSyncResponseFetchers.CACHE_AND_NETWORK) .enqueue(callback) }
取得したデータは RecyclerView
などに渡して画面に反映するのが典型的な使い方でしょう。
サブスクリプションを実装する
最後に、リアルタイムにミューテーションを通知してもらうためのサブスクリプションを実装します。 もうお気づきだと思いますが、これもまったく作法は同じです。
private fun subscription() { val subscription: OnCreateMessageSubscription = OnCreateMessageSubscription.builder().build() val callback = object : AppSyncSubscriptionCall.Callback<OnCreateMessageSubscription.Data> { override fun onFailure(e: ApolloException) { e.message?.let { toast(it) Log.e(TAG, it, e) } } override fun onResponse(response: Response<OnCreateMessageSubscription.Data>) { response.data()?.onCreateMessage()?.let { val item = ListMessagesQuery.Item( it.__typename(), it.id(), it.username(), it.content() ) addMessage(item) Log.i(TAG, "subscription onResponse: ${it.toString()}") } } override fun onCompleted() { Log.i(TAG, "subscription onCompleted") } } val subscriptionWatcher: AppSyncSubscriptionCall<OnCreateMessageSubscription.Data> = awsAppSyncClient.subscribe(subscription) subscriptionWatcher.execute(callback) }
新しいメッセージが追加されるごとに onResponse
がコールバックされるので、それを RecyclerView
が持つリストに追加して表示を更新するなどといった使い方が一般的でしょう。
サインアウトを実装
最後にサインアウトを実装します。
サインアウト処理自体は AWSMobileClient#signOut
メソッドを呼ぶだけです。
今回はオプションメニューに追加してみました。
override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.signOut -> { AWSMobileClient.getInstance().signOut( SignOutOptions.builder().invalidateTokens(true).build(), object : Callback<Void> { override fun onResult(result: Void?) { Log.i(TAG, "signOut(): onResult ok") } override fun onError(e: java.lang.Exception?) { Log.e(TAG, "signOut(): onResult error") } }) return true } else -> { return super.onOptionsItemSelected(item) } } }
サインアウトすると自動的に認証状態の変更を検知するリスナが発火されてサインイン画面が再び表示されるはずです。
まとめ
ちょうど30分ぐらいでしょうか!GraphQLがこれほど簡単に使えることに驚かれたのではないでしょうか。
AmplifyはバックエンドはすべてAWSのサービスであり、設定や組み合わせは柔軟でスケールもします。この使い勝手の良さとカスタマイズ性のバランスがAmplifyの大きな魅力だと個人的には考えています。
今日の例はごくごく初歩的な内容に留めましたが、今後いくつかのエントリでAmplifyを使ったネイティブアプリ開発の実践的な例をご紹介できたらなと考えています。楽しみにお待ち下さい!