KotlinでDynamoDBに接続 - その１

## 概要
AWS Cogniteを使って認証を行いDynamoDBに接続する簡易ログインアプリを作成しました。

:::note warn
プロジェクト全体のソースコードはこちらに置いてあります。
https://github.com/ist-h-i/LoginApp.git
:::
## AWSの設定
### DynamoDB
:::note warn
DynamoDBの詳細はこちら
https://aws.amazon.com/jp/dynamodb/
:::
テーブルを作成
![AWS DynamoDB1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/0eaef5dc-b73e-f4f5-d631-48a12d6ac062.png)

### SES
メールアドレスを登録
![AWS SES.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/8becc5b0-2698-de77-3d74-2cb0c582498a.png)

### Cognite User Pool
User Poolを作成
![AWS Cognignt UserPool1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/7dbebb94-4fb8-7028-0fee-c1780b01c3b1.png)

SESに登録したメールアドレスを送信元のEメールアドレスに設定
![AWS Cognignt UserPool2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/ef45fc98-26b6-fbe0-8ef6-9c2861eedb8b.png)

### Cognite ID Pool
ID Poolを作成
![AWS Cognignt IDPool1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/6d535d41-7f7f-175f-a072-53721ef43d27.png)
![AWS Cognignt IDPool2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/0455ed9a-b628-7c35-f3c6-93ff10acce74.png)

### Cognite IAM
ユーザアクセス > ゲストアクセス > ロールを編集
![AWS Cognignt IAM1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/cb867809-b61c-24c5-2279-47340c698a48.png)

新しいIAMを作成
![AWS Cognignt IAM2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/9bdf8380-18b5-9494-306e-c51829314b1e.png)

許可を追加 > インラインポリシーを作成
![AWS Cognignt IAM3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/485f11ae-4246-d0f8-99e7-4a54a9f49792.png)

アクセス許可にDynamoDBを指定
![AWS Cognignt IAM4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/cadc0d84-fe3c-9e06-7a77-2f6c8be2512b.png)

必要な機能を追加
![AWS Cognignt IAM5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/cd4a70c9-5f9e-4235-2458-afeffee9c5e2.png)

必要に応じてARNを設定
![AWS Cognignt IAM6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/304577fd-b9be-059a-7192-381b0340e55b.png)
![AWS Cognignt IAM7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/f03774e3-2da6-ceb3-bf5d-0be95688d1d9.png)

ロールをアクティブ化
![AWS Cognignt IAM8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/000c7293-e58f-0d6d-763d-51d9173af04b.png)

## アプリ実装
### MainActivity.kt
```kotlin
package com.example.loginapp

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.amazonaws.auth.CognitoCachingCredentialsProvider
import com.amazonaws.mobileconnectors.dynamodbv2.dynamodbmapper.DynamoDBMapper
import com.amazonaws.mobileconnectors.dynamodbv2.dynamodbmapper.DynamoDBMappingException
import com.amazonaws.mobileconnectors.dynamodbv2.dynamodbmapper.DynamoDBScanExpression
import com.amazonaws.regions.Region
import com.amazonaws.regions.Regions
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient
import com.amazonaws.services.dynamodbv2.model.AttributeValue
import com.example.loginapp.databinding.ActivityMainBinding
import com.example.loginapp.model.User

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 認証情報の取得
        val credentialsProvider = CognitoCachingCredentialsProvider(
            this, "ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
            Regions.AP_NORTHEAST_1 // Region
        )

        // DynamoDBクライアント
        val ddbClient: AmazonDynamoDB = AmazonDynamoDBClient(credentialsProvider)
        // デフォルトではUS-EASTリージョンでクライアント作成されてしまうため明示的にAP_NORTHEASTに設定
        ddbClient.setRegion(Region.getRegion(Regions.AP_NORTHEAST_1))
        val ddbMapper = DynamoDBMapper.builder().dynamoDBClient(ddbClient).build()

        // レイアウト紐付け
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // ログインボタン押下時の設定
        binding.loginButton.setOnClickListener {
            val userId: String = binding.userId.text.toString()
            val password: String = binding.password.text.toString()

            // 未入力欄がある場合
            if (userId.isBlank() || password.isBlank()) {
                showLongToast(R.string.inputUserInfo)
                return@setOnClickListener
            }

            // DynamoDBへのQuery属性
            val eav = HashMap<String, AttributeValue>()
            eav[":v1"] = AttributeValue().withS(userId)
            eav[":v2"] = AttributeValue().withS(password)

            val countExpression =
                DynamoDBScanExpression().withFilterExpression("UserId = :v1 AND Password = :v2")
                    .withExpressionAttributeValues(eav)

            // 外部通信のためスレッド生成
            val queryDynamoDb = Thread {
                try {
                    // Query結果
                    val itemCount: Int = ddbMapper.count(User::class.java, countExpression)
                    if (itemCount != 0) {
                        // User情報が登録済みの場合
                        showLongToast(R.string.loginSuccess)
                        // ログイン後後続処理はこの下に記載
                    } else {
                        // User情報がない場合
                        showLongToast(R.string.loginFailed)
                    }
                } catch (error: DynamoDBMappingException) {
                    showLongToast(R.string.unknownError)
                }
            }
            queryDynamoDb.start()
            try {
                queryDynamoDb.join()
            } catch (error: InterruptedException) {
                Toast.makeText(this, R.string.unknownError, Toast.LENGTH_LONG).show()
            }

        }
    }

    private fun showLongToast(messageInt: Int) {
        // 別スレッドからUIを操作
        val handler = Handler(Looper.getMainLooper())
        handler.post {
            Toast.makeText(this, messageInt, Toast.LENGTH_LONG).show()
        }
    }
}
```

### activity_main.xml
drawableのリソース等は以下参照
https://github.com/ist-h-i/LoginApp.git

```xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="30dp"
        app:cardCornerRadius="30dp"
        app:cardElevation="20dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:background="@drawable/custom_edittext"
            android:orientation="vertical"
            android:padding="24dp">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/login"
                android:textAlignment="center"
                android:textColor="@color/lavender"
                android:textSize="@dimen/text_large"
                android:textStyle="bold" />

            <EditText
                android:id="@+id/userId"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_marginTop="40dp"
                android:autofillHints="username"
                android:background="@drawable/custom_edittext"
                android:drawableStart="@drawable/ic_baseline_person_24"
                android:drawablePadding="8dp"
                android:hint="@string/userId"
                android:inputType="textPersonName"
                android:padding="8dp"
                android:textColor="@color/black"
                android:textColorHint="@color/lavender_80" />

            <EditText
                android:id="@+id/password"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_marginTop="20dp"
                android:autofillHints="password"
                android:background="@drawable/custom_edittext"
                android:drawableStart="@drawable/ic_baseline_lock_24"
                android:drawablePadding="8dp"
                android:hint="@string/password"
                android:inputType="textPassword"
                android:padding="8dp"
                android:textColor="@color/black"
                android:textColorHint="@color/lavender_80" />

            <Button
                android:id="@+id/loginButton"
                android:layout_width="match_parent"
                android:layout_height="60dp"
                android:layout_marginTop="30dp"
                android:backgroundTint="@color/lavender"
                android:text="@string/login"
                android:textSize="@dimen/text_middle"
                app:cornerRadius="@dimen/button_corner_radius" />

        </LinearLayout>
    </androidx.cardview.widget.CardView>
</LinearLayout>
```
## まとめ
今回はDynamoDBに接続してみました。
DynamoDBテーブルはスキーマレスであるため、テーブルを作成する際にプライマリキーを除いて属性やデータ型を定義する必要がなく内容の変更が非常に容易です。一方、データの一貫性が担保されないため、エラーハンドリングをアプリ側で徹底するように心がけています。
DBにアクセスできるとやれることの幅が非常に大きくなるので様々なデータベースを使いこなしていきたいです。