AndroidアプリにAws Amplify 無しで Aws AppSyncを導入する際の備忘録

# 概要
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プロジェクトの生成などです。

今回、同期したいデータはこんな感じのデータだとします。

```graphql:.graphql
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を追加

```gradle:build.gradle
    dependencies {
        ~~~~~~
        //↓これを追加
        classpath 'com.amazonaws:aws-android-sdk-appsync-gradle-plugin:2.10.1'
    }
```

### moduleの`build.gradle`

moduleの`build.gradle`にライブラリの読み込みと、適応を追加

```gradle: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で使用する外部サービスを追加します。

```xml:AndroidManifest.xml
    <application>
~~

        <service android:name="org.eclipse.paho.android.service.MqttService" />
~~
    </application>
```


## AppSyncの必要ファイルをDL
### 接続情報設定ファイルのダウンロード

AwsコンソールのAppSyncのそれぞれのプロジェクトに進むと、「アプリと統合する」という項目があります。
ここで、デフォルトだと、Amplifyでの統合を勧められますが、その下にあるオレンジ色の「設定をダウンロード」から接続情報ファイルをダウンロードします。
ファイル名は`awsconfiguration.json`です。


```json: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ファイルに配置します。
![スクリーンショット 2020-03-11 14.42.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45441/b55aa1a0-4410-f1d7-c811-c37dc7cfb60d.png)

ProductionとStaging等、環境ごとに分けたい場合には、Build Valiantsごとにリソースファイルを分けるのと同じ方法で、複数の設定ファイルを切り替えることができます。


### スキーマ定義の配置
このファイルが一番どこに配置すれば良いかわからなくて困りました。

正解は、src->mainの下に、`graphql`フォルダを作り、その下にさらにフォルダを作っていく形になります。
![スクリーンショット 2020-03-11 14.56.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45441/98daa786-f899-0101-d500-69086ff3450b.png)

`graphql`フォルダより下のフォルダ構成が、そのまま、スキーマで定義されているクラスのパッケージになるので、わかりやすいフォルダ構成にしておくのが良いと思います。
上記の構成だと、`com.example.ClassName`となります。

### .graphql ファイルの作成
AppSyncに対してQueryをかけたり、データの更新の監視を行う機能を定義します。

例えば、下記のように書けば、指定したIDを持ったUser情報に更新があると、コールバック形式でデータが流れる機能と、年齢で絞ったUserデータの一覧を取得する機能を作れます。
ここでの`subscription user($id: String!)`とかは、メソッド名のように見えて、自動で生成されるクラスのクラス名のPrefixとコンストラクタの引数になります。

注意点は、ここで指定するメソッド名(userUpdatesやgetUsers)・引数の形式は、AppSyncで設定したメソッドと揃えないと駄目です。


```graphql:example.graphql
# データ監視
subscription user($id: String!){
    userUpdates(id: $id){
        id
        user_name
        age
        status
    }
}

query getUsersByAge($age: String!){
    getUsers(age: $age){
        id
        user_name
        age
        status
    }
}

```

## 実際の呼び出し

実際の更新検知は下記の手順で行います。
1. AWSへアクセスするための認証情報の初期化
2. 1の認証情報を用いてAWSAppSyncClientを初期化
3. 2のAWSAppSyncClientに対して、情報の更新を受け取るAppSyncSubscriptionCallを初期化
4. 3のAppSyncSubscriptionCallに対して、コールバックを登録

AWSの認証にIAMを使用する場合にはBasicAWSCredentialsクラスを用いて、`Access key ID`と`Secret access key`を渡すことで一番簡単に認証させられます。
BasicAWSCredentialsクラスを用いる場合には、credentialsProviderにはStaticCredentialsProviderを指定します。

```kotlin
    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の初期化までは一緒です。

```kotlin

    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*を要求されています。
~~どうして・・・どうして・・・🙀~~

```Java:SubscriptionAuthorizer.java
    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から帰ってくるデータは単体のデータとなるので、データのパースができず、エラーが起きます。

#### 参考
```graphql:.graphql
~~ 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]("https://github.com/timqian/gql-generator")