NCMBのKotlin SDKを使ってTodoアプリを作ろう

NCMBでは公式SDKとしてSwift/Objective-C/Kotlin/Java/Unity/JavaScript SDKを用意しています。また、それ以外にもコミュニティSDKとして、非公式ながらFlutter/React Native/Google Apps Script/C#/Ruby/Python/PHPなど幅広い言語向けにSDKが開発されています。

今回は公式SDKの一つ、Kotlin SDKを使ってTodoアプリを作ってみます。まず画面の仕様とSDKの初期化について解説します。

## 完成版のコード

作成したデモアプリのコードは[NCMBMania/Kotlin_Todo: Kotlin SDKを使ったTodoアプリです](https://github.com/NCMBMania/Kotlin_Todo)にアップロードしてあります。

## ベースアプリのダウンロードまで

<iframe width="560" height="315" src="https://www.youtube.com/embed/l3Uc01MmC3Y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

### ベースについて

今回はKotlin + Jetpack Composeの組み合わせになっています。

[NCMBMania/Kotlin_Todo_Handson](https://github.com/NCMBMania/Kotlin_Todo_Handson)

## NCMB SDKのインストール

<iframe width="560" height="315" src="https://www.youtube.com/embed/hbbq-CyW3Eg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

NCMB SDKは[Releases · NIFCLOUD-mbaas/ncmb_kotlin](https://github.com/NIFCLOUD-mbaas/ncmb_kotlin/releases)よりダウンロードします。Zipファイルをダウンロードして展開すると、NCMB.jarというファイルが取得できます。このファイルをKotlinプロジェクトの `app/libs` 以下にコピーします。

`app/build.gradle` を開いて編集します。そして以下の4つを追加します。一番下のkotlinx-coroutinesを追加すると、保存処理などが同期的に処理できるようになります。

```
dependencies {
	// 省略
  implementation 'com.squareup.okhttp3:okhttp:4.8.1'
	implementation 'com.google.code.gson:gson:2.3.1'
	api files('libs/NCMB.jar')
	implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
}
```

編集したら同期します。これでNCMB SDKの利用準備が整います。

## Composeについて

<iframe width="560" height="315" src="https://www.youtube.com/embed/ISKJmYx5zZ4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

今回は以下のComposeを用意しています。

- Navigation
- FormScreen
- ListScreen
- TaskRow
- DetailScreen

### Navigation

各画面への遷移をハンドリングしています。Navigationコンポーネントを利用しています。

### FormScreen

![Screenshot_20220414_155917.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f956bebd-4fa5-230a-c9a8-5e1fe838894c.png)


タスクの入力フォームです。新規登録、編集の両方で利用しています。

### ListScreen

![Screenshot_20220414_155935.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/535bde76-e711-1500-07e2-a2d641947b09.png)


タスクを一覧表示します。各行のデータはTaskRowにて表示しています。

### TaskRow

タスクの一覧表示で、各行を表示するのに使います。

### DetailScreen

![Screenshot_20220414_155946.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f2449883-9409-21d1-bbbe-90fd9531ddab.png)

一覧表示で選択された、タスクの詳細表示を行います。

### コードの編集

MainActivity.ktを開いてNCMBの初期化を行います。 `super.onCreate` の下でNCMBを初期化します。 `YOUR_APPLICATION_KEY` と `YOUR_CLIENT_KEY` はそれぞれあなたのものと書き換えてください。

```kotlin
// 1. NCMBをインポート
import com.nifcloud.mbaas.core.NCMB

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 2. NCMBを初期化
        NCMB.initialize(
            this,
            "YOUR_APPLICATION_KEY",
            "YOUR_CLIENT_KEY"
        )
        setContent {
						// 省略
        }
    }
}
```

これでNCMBの初期化が終わり、利用準備が整います。

## タスク保存処理の実行

<iframe width="560" height="315" src="https://www.youtube.com/embed/ZW3EmqUHy3g" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

### Navigation.ktの修正

`Navigation.kt` にて、NCMB SDKをインポートします。

```kotlin
// 1. NCMBObjectをインポートします
import com.nifcloud.mbaas.core.NCMBObject
```

次に `var obj = 0` をNCMBObjectに変更します。

```kotlin
// 2. 新しいNCMBObjectを作成
val obj = NCMBObject(className)
```

次の修正に入る前に、 Util.kt に関数を追加します。

### Util.ktについて

文字列をNCMBObjectに戻す関数 `strToNCMBObject` と JSONObjectをNCMBAclに変換する関数 `getAcl` を用意します。まずSDKをインポートします。

```kotlin
// 1. NCMBAcl/NCMBObjectを読み込みます
import com.nifcloud.mbaas.core.NCMBObject
import com.nifcloud.mbaas.core.NCMBAcl
```

strToNCMBObjectの内容です。画面遷移する際に受け取った文字列をJSONObjectにし、それをNCMBObjectのフィールド（DBで言うカラム相当）にセットしていきます。

objectIdはNCMBのデータ管理におけるユニークキー、createDateは作成日時、updateDateは更新日時、aclはアクセス権限に関するフィールドになります。これらはNCMBの規定フィールドです。className（クラス名）はDBで言うところのテーブル名相当になります。

```kotlin
// 2. 文字列をNCMBObjectに復元する関数です
fun strToNCMBObject(str: String, className: String): NCMBObject {
    val json = JSONObject(str)
    val obj = NCMBObject(className = className)
    json.keys().forEach { key ->
        when (key) {
            "objectId" -> {
                obj.setObjectId(json.get(key) as String)
            }
            "createDate" -> {
                obj.setCreateDate(getDate(json.get(key) as String))
            }
            "updateDate" -> {
            } // obj.put("updateDate", getDate(json.get(key) as String))}
            "acl" -> {
                obj.setAcl(getAcl(json.get(key) as JSONObject))
            }
            else -> obj.put(key, json.get(key))
        }
    }
    return obj
}
```

上記関数で呼ばれている `getAcl` の内容です。ACLは誰がデータにアクセスできるかを指定するものです。NCMBでは以下の条件に対して、読み書き設定ができます。

- 誰でも
- 特定のユーザー
- 特定のグループ

```kotlin
// 3. NCMBAclを復元する関数です
fun getAcl(obj: JSONObject): NCMBAcl {
    val acl = NCMBAcl()
    obj.keys().forEach { key ->
        val map = obj.get(key) as JSONObject
        when {
            key == "*" -> {
                if (!map.isNull("read") && map.get("read") as Boolean) {
                    acl.publicReadAccess = true
                }
                if (!map.isNull("write") && map.get("write") as Boolean) {
                    acl.publicWriteAccess = true
                }
            }
            key.indexOf("role:") > -1 -> {
                val match = "role:(.*)$".toRegex().find(key)
                val roleName = match?.groups?.get(1)?.value
                if (roleName != null) {
                    if (!map.isNull("read") && map.get("read") as Boolean) {
                        acl.setRoleReadAccess(roleName, true)
                    }
                    if (!map.isNull("write") && map.get("write") as Boolean) {
                        acl.setRoleWriteAccess(roleName, true)
                    }
                }
            }
            else -> {
                if (!map.isNull("read") && map.get("read") as Boolean) {
                    acl.setUserReadAccess(key, true)
                }
                if (!map.isNull("write") && map.get("write") as Boolean) {
                    acl.setUserWriteAccess(key, true)
                }
            }
        }
    }
    return acl
}
```

### Navigation.ktの修正（2）

関数が準備できたので、 `Navigation.kt` の修正に戻ります。

```kotlin
// 4. 受け取った文字列からNCMBObjectを復元します
val obj = strToNCMBObject(backStackEntry.arguments!!.getString("task")!!, className)
```

### FormScreen.ktの修正

NCMB SDKをインポートします

```kotlin
// 1. NCMBExceptionとNCMBObjectを読み込みます
import com.nifcloud.mbaas.core.NCMBException
import com.nifcloud.mbaas.core.NCMBObject
```

元々はエラーを出さないためにAnyにしていたのをNCMBObjectに直します。

```kotlin
// 2. AnyをNCMBObjectに変更します
fun FormScreen(navController: NavController, obj: Any) {

// ↓ 修正後
fun FormScreen(navController: NavController, obj: NCMBObject) {
```

インスタンス変数を設定します。今回はTodoのタイトル（title）と本文（body）をデフォルト値として受け取ります。これは編集時を想定してのことです。

```kotlin
// 3. objectIdを入れます
var objectId = obj.getObjectId();
// 4. titleをrememberで定義します。デフォルト値はNCMBObjectのtitleです
var title by remember { mutableStateOf(if (objectId != null) obj.getString("title")!! else "") }
// 5. bodyをrememberで定義します。デフォルト値はNCMBObjectのbodyです
var body by remember { mutableStateOf(if (objectId != null) obj.getString("body")!! else "") }
```

#### 保存処理

保存を実行する `save` メソッドは次のようになります。関数全体を紹介します。

```kotlin
// 保存処理
val save = {
		progress = true
		// 6. 保存処理を記述します
		try {
				obj.put("title", title)
				obj.put("body", body)
				obj.save()
				navController.navigate("list")
		} catch(e: NCMBException){
				Log.d("INFO",  e.message)
				message = "タスクが保存できませんでした"
				showDialog = true
		}
		progress = false
}
```

## 一覧画面の作成

<iframe width="560" height="315" src="https://www.youtube.com/embed/bJxKntvdJfE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

### ListScreen.ktの編集

NCMB SDKを読み込みます。

```kotlin
// 1. NCMBCallback/NCMBObject/NCMBQueryを読み込みます
import com.nifcloud.mbaas.core.NCMBCallback
import com.nifcloud.mbaas.core.NCMBObject
import com.nifcloud.mbaas.core.NCMBQuery
```

NCMBからデータを取得する際には `NCMBQuery` クラスを使います。 `NCMBQuery.forObject` で指定するクラス名は、NCMBのデータストアにあるクラス名になります。データを取得後、 `remember` で定義した `ary` に結果を入れることで、画面描画に使われます。

results は `List<dynamc>` なので、 `List<NCMBObject>` にキャストしています。

```kotlin
// 2. aryをrememberのNCMBObjectのListで定義します。デフォルトは空です
var ary = remember { mutableStateOf<List<NCMBObject>>(emptyList()) }
// 3. NCMBQueryを定義します。クラス名はTaskです。
val query = NCMBQuery.forObject("Task")
// 4. 検索を実行して、結果をaryに格納します
query.findInBackground(NCMBCallback { e, results ->
		if (e == null) {
				ary.value = results as List<NCMBObject>
		}
})
```

元々 `ary` をただの配列で定義しています（エラー防止のため、単なる配列でした）。 `remember` にしたので、利用法を変えます。

```kotlin
// 5. ary -> ary.value に書き直してください
items(ary) { obj ->

// ↓
items(ary.value) { obj ->
```

### TaskRow.kt の修正

NCMB SDKを読み込みます。

```kotlin
// 1. NCMBObjectを読み込みます
import com.nifcloud.mbaas.core.NCMBObject
```

`Any` 型だったのを `NCMBObject` にします（エラー防止のため、Any型にしてました）。

```kotlin
// 2. Any -> NCMBObjectにします
fun TaskRow(obj: Any, navController: NavController) {

// ↓
fun TaskRow(obj: NCMBObject, navController: NavController) {
```

タスクのタイトルを画面に表示します。

```kotlin
// 4. NCMBObjectのtitleを表示します
Text(obj.getString("title")!!, fontSize = 25.sp)
```

## タスクをタップした後の処理の実装

<iframe width="560" height="315" src="https://www.youtube.com/embed/JxiVfcsOBGQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

### TaskRow.kt の修正

詳細画面に遷移する際にはNCMBObjectを文字列にして送らないといけません。そこで、JSONObjectを使ってNCMBObjectを文字列化します。

```kotlin
// 3. NCMBObjectを文字列化します
obj.keys.forEach {key ->
		json.put(key, obj.get(key)!!)
}
```

### Navigation.ktの修正

`detail/task={task}` のルーティングでも、文字列をNCMBObjectに戻す処理にします。

```kotlin
// 3. 受け取った文字列からNCMBObjectを復元します
val obj = strToNCMBObject(backStackEntry.arguments!!.getString("task")!!, className)
```

### DetailScreen.ktの修正

NCMB SDKを読み込みます。

```kotlin
// 1. NCMBObjectをインポート
import com.nifcloud.mbaas.core.NCMBObject
```

Any型をNCMBObjectにします。

```kotlin
// 2. Any -> NCMBObjectに変更します
fun DetailScreen(navController: NavController, obj: Any) {

// ↓
fun DetailScreen(navController: NavController, obj: NCMBObject) {
```

受け取ったNCMBObjectを画面に表示します。

```kotlin
// 5. NCMBObjectのtitleを表示します
Text(text = obj.getString("title")!!,
		fontSize = 35.sp,
		modifier = Modifier.padding(5.dp)
)
// 6. NCMBObjectのbodyを表示します
Text(text = obj.getString("body")!!,
		fontSize = 30.sp,
		modifier = Modifier.padding(5.dp)
)
```

ゴミ箱アイコンをタップした際のイベントは、NCMBObjectの削除処理です。

```kotlin
// 4. NCMBObjectを削除します
obj.delete()
```

編集アイコンをタップした際のイベントは、NCMBObjectを文字列化した上で `edit/task` へ遷移します。

```kotlin
// 3. NCMBObjectを文字列化します
obj.keys.forEach {key ->
		json.put(key, obj.get(key)!!)
}
```

## まとめ

ここまででNCMBのデータストアを使ったTodoアプリ開発は完了となります。NCMBには他にも認証やファイル保存機能、プッシュ通知などがあります。ぜひあなたのアプリ開発に役立ててください。

[mBaaSでサーバー開発不要！ | ニフクラ mobile backend](https://mbaas.nifcloud.com/)