概要
Android アプリに Aws AppSyncを導入する方法とTipsをメモしておきます(2020/3/10時点)
前提
- AWS側の認証はIAM
- Androidアプリはネイティブ(React等ではない)タイプ
- Aws Amplifyは使わない
- すでに既存のAndroidアプリが存在していて、それに追加する形
今回、すでに存在するAndroidアプリに、すでに存在するDynamoDBのデータを自動でSyncさせたいという要求のためにあえてAmplifyは使わずに、AppSyncのみの機能を使う方法です。
新規でまるっと作れるなら、Amplify使ってやったほうが良いと思います。
いろいろ調べましたが、多分すでAndroidアプリ&Sync元データが存在する状況下てAmplifyを使って上手く安全に工数少なくやる方法はないと思います。
この記事で紹介しないこと
- AWS上での設定の詳細
- GraphQLの説明
導入手順
0. AWSコンソール上での設定
それぞれの状況下で違うと思うので割愛
AppSyncへのアクセス権を持ったIAMの登録やAppSyncプロジェクトの生成などです。
今回、同期したいデータはこんな感じのデータだとします。
type User {
id: String!
user_name: String!
age: String!
status: String!
}
type Mutation {
updateUser(user: UserInput!): User
registerUser(user: RegisterUserInput!): User
}
type Query {
getUsers(age: String!): [User]
}
input RegisterUserInput {
id: String!
user_name: String!
age: String!
}
type Subscription {
userUpdates(id: String!): User
@aws_subscribe(mutations: ["updateUser"])
}
input UserInput {
id: String!
user_name: String!
age: String!
status: String!
}
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
1. AndroidアプリにAppSyncライブラリを追加
まずは、AndroidアプリにAppSyncが使用できるようにライブラリの追加をします。
プロジェクトレベルのbuild.gradle
プロジェクトレベルのbuild.gradle
にclasspathを追加
dependencies {
~~~~~~
//↓これを追加
classpath 'com.amazonaws:aws-android-sdk-appsync-gradle-plugin:2.10.1'
}
moduleのbuild.gradle
moduleのbuild.gradle
にライブラリの読み込みと、適応を追加
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.amazonaws.appsync'
// ↑追加する
~省略~
dependencies {
// ここから
//Base SDK
implementation 'com.amazonaws:aws-android-sdk-core:2.16.8'
//AppSync SDK
implementation 'com.amazonaws:aws-android-sdk-appsync:2.10.1'
implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.0'
implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
// ここまで追加する
}
なんで、最新バージョンではなく2系を使用してるのかは最後に記載しています。
AndroidManifestにSync用Service追加
AndroidManifestにAppSyncで使用する外部サービスを追加します。
<application>
~~
<service android:name="org.eclipse.paho.android.service.MqttService" />
~~
</application>
AppSyncの必要ファイルをDL
接続情報設定ファイルのダウンロード
AwsコンソールのAppSyncのそれぞれのプロジェクトに進むと、「アプリと統合する」という項目があります。
ここで、デフォルトだと、Amplifyでの統合を勧められますが、その下にあるオレンジ色の「設定をダウンロード」から接続情報ファイルをダウンロードします。
ファイル名はawsconfiguration.json
です。
{
"UserAgent": "aws-amplify-cli/0.1.0",
"Version": "1.0",
"IdentityManager": {
"Default": {}
},
"AppSync": {
"Default": {
"ApiUrl": "https://xxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql",
"Region": "ap-northeast-1",
"AuthMode": "AWS_IAM",
"ClientDatabasePrefix": "yyyyyyyyy_AWS_IAM"
}
}
}
スキーマ定義のダウンロード
AppSync->スキーマ->スキーマをエクスポート から、スキーマ定義をダウンロードします。
ここで必要なのはJsonファイルだけです。
GraphQLのファイルもダウンロードできますが、Androidアプリには必要ないです。
必要ファイルの配置
上でダウンロードしたファイルを配置していきます。
ここがドキュメントを読んでもよくわからなかったので非常に面倒です。
awsconfiguration.jsonの配置
awsconfiguration.json
はres->rawファイルに配置します。
ProductionとStaging等、環境ごとに分けたい場合には、Build Valiantsごとにリソースファイルを分けるのと同じ方法で、複数の設定ファイルを切り替えることができます。
スキーマ定義の配置
このファイルが一番どこに配置すれば良いかわからなくて困りました。
正解は、src->mainの下に、graphql
フォルダを作り、その下にさらにフォルダを作っていく形になります。
graphql
フォルダより下のフォルダ構成が、そのまま、スキーマで定義されているクラスのパッケージになるので、わかりやすいフォルダ構成にしておくのが良いと思います。
上記の構成だと、com.example.ClassName
となります。
.graphql ファイルの作成
AppSyncに対してQueryをかけたり、データの更新の監視を行う機能を定義します。
例えば、下記のように書けば、指定したIDを持ったUser情報に更新があると、コールバック形式でデータが流れる機能と、年齢で絞ったUserデータの一覧を取得する機能を作れます。
ここでのsubscription user($id: String!)
とかは、メソッド名のように見えて、自動で生成されるクラスのクラス名のPrefixとコンストラクタの引数になります。
注意点は、ここで指定するメソッド名(userUpdatesやgetUsers)・引数の形式は、AppSyncで設定したメソッドと揃えないと駄目です。
# データ監視
subscription user($id: String!){
userUpdates(id: $id){
id
user_name
age
status
}
}
query getUsersByAge($age: String!){
getUsers(age: $age){
id
user_name
age
status
}
}
実際の呼び出し
実際の更新検知は下記の手順で行います。
- AWSへアクセスするための認証情報の初期化
- 1の認証情報を用いてAWSAppSyncClientを初期化
- 2のAWSAppSyncClientに対して、情報の更新を受け取るAppSyncSubscriptionCallを初期化
- 3のAppSyncSubscriptionCallに対して、コールバックを登録
AWSの認証にIAMを使用する場合にはBasicAWSCredentialsクラスを用いて、Access key ID
とSecret access key
を渡すことで一番簡単に認証させられます。
BasicAWSCredentialsクラスを用いる場合には、credentialsProviderにはStaticCredentialsProviderを指定します。
private var appSyncSubscriptionCall: AppSyncSubscriptionCall<UserSubscription.Data>? = null
private val basicAWSCredentials: AWSCredentials by lazy {
BasicAWSCredentials("Access key ID", "Secret access key")
}
private val awsAppSyncClient: AWSAppSyncClient by lazy {
AWSAppSyncClient.builder()
.credentialsProvider(StaticCredentialsProvider(basicAWSCredentials))
.awsConfiguration(AWSConfiguration(context))
.context(context)
.defaultResponseFetcher(AppSyncResponseFetchers.NETWORK_FIRST)
.build()
}
private fun initAppSyncSubscription(id: String){
appSyncSubscriptionCall = awsAppSyncClient.subscribe(UserSubscription(id))
}
private fun subscribe {
appSyncSubscriptionCall!!.execute(object : AppSyncSubscriptionCall.Callback<UserSubscription.Data> {
override fun onFailure(e: ApolloException) {
}
override fun onResponse(response: Response<UserSubscription.Data>) {
// ここがUserデータに更新があると呼ばれる
}
override fun onCompleted() {
}
})
}
クエリについては、AWSAppSyncClientの初期化までは一緒です。
private var appSyncQueryCall: AppSyncQueryCall<GetUsersByAgeQuery.Data>? = null
private fun query(age: String){
appSyncQueryCall = awsAppSyncClient.query(GetUsersByAgeQuery(age))
appSyncQueryCall.enqueue(object : GraphQLCall.Callback<GetUsersByAgeQuery.Data>() {
override fun onFailure(e: ApolloException) {
}
override fun onResponse(response: Response<GetUsersByAgeQuery.Data>) {
for (status in response.data()!!.users!!) {
// ここに取得できたデータが流れてくる
}
}
})
}
それぞれで使用するその環境独自のクラス(今回だとUserSubscriptionクラスだとかGetUsersByAgeQueryクラス)は、ローカルのgradlqlファイルで設定した文字列+QueryやSubscriptionになります。
ハマリポイント
認証にIAMを使う場合には最新版のSDKを使ってはいけない
AWS側の認証にIAMを使いたい方は必ずSDKの2系を使ってください。最新版の3系を使用すると、IAMを使う設定をしても、なぜがCognitoでの認証になってしまいます。
問題になる箇所はAppSyncSDKのSubscriptionAuthorizerクラスのgetAuthorizationDetailsForIAMメソッドで何故かCognitoIdentityを要求されています。
どうして・・・どうして・・・🙀
private static JSONObject getAuthorizationDetailsForIAM(boolean connectionFlag, AWSConfiguration awsConfiguration,
Subscription subscription,
Context applicationContext) throws JSONException {
String identityPoolId;
String regionStr;
try {
JSONObject identityPoolJSON = awsConfiguration.optJsonObject("CredentialsProvider")
.getJSONObject("CognitoIdentity")
.getJSONObject(awsConfiguration.getConfiguration());
identityPoolId = identityPoolJSON.getString("PoolId");
regionStr = identityPoolJSON.getString("Region");
データの更新をチェックするSubscriptionのメソッドの返り値に配列は指定できない
AWS側で設定する、Subscriptionのメソッドの返り値に配列を指定してはだめです。
しかも厄介なことに、配列を指定してもAWSコンソール側等ではエラーが出ません。
しかし、AWSのAppSyncコンソールからダウンロードできるJsonでは返り値に配列が指定されますが、SDKのCallbackから帰ってくるデータは単体のデータとなるので、データのパースができず、エラーが起きます。
参考
~~ OKなのはこれ
type Subscription {
userUpdates(id: String!): User
@aws_subscribe(mutations: ["updateUser"])
}
~~
~~ これだとだめ
type Subscription {
userUpdates(id: String!): [User]
@aws_subscribe(mutations: ["updateUser"])
}
~~
基本的にデータは非同期で流れてくる
subscribeは当たり前ですが、データの検索のQueryも結果が非同期で流れてきます。
よく考えれば当然なのですが、Androidで使用する場合には、ちゃんとアプリのライフサイクルに合わせて、AppSyncQueryCallやAppSyncSubscriptionCallのcancelメソッドを呼び出す必要があります。
ローカルのgraphqlファイルの文法間違いが難しい
ローカルに有るgraphqlファイルの文法に間違いがあっても、とりあえずビルドのエラーしかでず、具体的な間違いがわかりません。
人の手で書くのは辛いので、これらを使うのが良いです。
gql-generator