【ハンズオン資料】NCMBのKotlin SDKでカメラメモアプリを作る

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を使ってカメラメモアプリを作ってみます。

## ベースコード

下記URLからコードをダウンロード、またはgit cloneしてください。

```
https://github.com/NCMBMania/kotlin-camera-memo-handson
```

## ベースについて

今回はKotlin + Jetpack Composeの組み合わせになっています。また、NCMB利用部分以外のコードは記述済みです。

## Composeについて

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

- MemoBottomNavigation
- FormScreen
- ListScreen
- ListRow

### MemoBottomNavigation

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/73d69197-25d2-e834-9ff6-273990d468f4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/73d69197-25d2-e834-9ff6-273990d468f4.png)

画面下に表示されます。入力フォーム（FormScreen）と一覧画面（ListScreen）を読み込んでいます。

### FormScreen

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/3aa96c81-e719-bf1d-d5bb-038d944c7d0d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/3aa96c81-e719-bf1d-d5bb-038d944c7d0d.png)

入力フォームです。写真を選択して表示したり、写真と紐付けるメモ（テキスト情報）をNCMBに保存します。

### ListScreen

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/4d2934af-1d12-494a-c2ab-367d0474c68f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/4d2934af-1d12-494a-c2ab-367d0474c68f.png)

一覧画面です。NCMBからメモ一覧データを取得します。取得したデータはListRowで表示します。

### ListRow

入力されたメモを表示し、ファイルストアから写真データをダウンロードして表示します。

## NCMB SDKのインストール

**ハンズオンのコードではすでにインストール済みです。今後の参考にしてください。**

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

これでNCMB SDKの利用準備が整います。

## NCMB SDKの初期化

**ハンズオンのコードではすでに設定済みです。今後の参考にしてください。**

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

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

```

編集したら同期します。

### コードの編集

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

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

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

```

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

## ナビゲーションバーの読み込み

ナビゲーションバーのコードは以下のようになります。Form（入力画面）とList（一覧画面）があります。デフォルトは入力画面になります。

```kotlin
// 記述済み
// タブ用のクラス
sealed class Item(var dist: String, var icon: ImageVector) {
    object Form : Item("Form", Icons.Rounded.AddBox)
    object List : Item("List", Icons.Rounded.List)
}

@Composable
fun MemoBottomNavigation() {
    // 選択されたタブの管理用
    var selectedItem = remember { mutableStateOf(0) }
    // タブ
    val items = listOf(Item.Form, Item.List)
    // ナビゲーションコントローラー
    val navController = rememberNavController()
    KotlinFirstDemoTheme {
        Surface(color = MaterialTheme.colors.background) {
            Scaffold(
                // 画面下に表示
                bottomBar = {
                    // ナビゲーションバーの表示
                    BottomNavigation {
                        items.forEachIndexed { index, item ->
                            BottomNavigationItem(
                                icon = { Icon(item.icon, contentDescription = item.dist) },
                                label = { Text(item.dist) },
                                selected = selectedItem.value == index,
                                onClick = {
                                    navController.navigate(item.dist)
                                }
                            )
                        }
                    }
                }
            ) {
                // ナビゲーション情報の設定
                NavHost(navController = navController, startDestination = "Form") {
                    composable("Form") { FormScreen()}
                    composable("List") { ListScreen()}
                }
            }
        }
    }
}

```

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/ecb7bfab-b1c5-bccc-66d4-8bb37d43f7ee.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/ecb7bfab-b1c5-bccc-66d4-8bb37d43f7ee.png)

なお、アイコンが不足しているので `app/build.gradle` にてライブラリを追加します。

```
// 記述済み
implementation "androidx.compose.material:material-icons-extended:$compose_version"

```

このナビゲーションバーはMainActivityにて読み込みます。

```kotlin
// 記述済み
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // NCMBの初期化
        NCMB.initialize(
            this.getApplicationContext(),
            "YOUR_APPLICATION_KEY",
            "YOUR_CLIENT_KEY"
        )
        setContent {
            // ナビゲーションバーの表示
            MemoBottomNavigation()
        }
    }
}

```

## 入力画面について

入力画面は以下のような構成になります。長いですが、重要なのはボタンを押すと `save` 関数が呼ばれることです。他は画像が選ばれると、デフォルトのアイコン表示と差し替わるのと、アラート表示を用意していることくらいでしょう。

```kotlin
// 記述済み
@RequiresApi(Build.VERSION_CODES.P)
@Composable
fun FormScreen() {
    val context = LocalContext.current
    // 選択した画像のURIが入る
    var imageUri = remember { mutableStateOf<Uri?>(null) }
    // 選択した画像のbitmapが入る
    var bitmap by remember { mutableStateOf<Bitmap?>(null) }
    // 画像選択時の処理
    val launcher = rememberLauncherForActivityResult(contract =
    ActivityResultContracts.GetContent()) { uri: Uri? ->
        if (uri != null) {
            imageUri.value = uri
            val source = ImageDecoder.createSource(context.contentResolver, uri!!)
            bitmap = ImageDecoder.decodeBitmap(source)
        }
    }
    // アラート表示
    var showDialog by remember { mutableStateOf(false) }
    if (showDialog) {
        AlertDialog(
            onDismissRequest = {},
            buttons = {
                Button(onClick = {
                    showDialog = false
                }) {
                Text("OK")
                }
            },
            title = {Text("保存完了")},
            text = {Text("保存完了しました")}
        )
    }
    // 入力するメモ
    var memo by remember { mutableStateOf("") }
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // 画像が選択されているかどうかで表示の出し分け
        if (imageUri.value == null) {
            // 選択していない場合はアイコンを表示
            IconButton(
                onClick = {
                    launcher.launch("image/*")
                },
                modifier = Modifier.size(300.dp)
            ) {
                Icon(Icons.Rounded.Image, "デフォルトアイコン", Modifier.size(size = 300.dp))
            }
        } else {
            // 選択している場合はサムネイル表示
            Image(
                bitmap = bitmap!!.asImageBitmap(),
                contentDescription = "選択した写真",
                Modifier.clickable(
                    onClick = {
                        launcher.launch("image/*")
                    }
                )
            )
        }
        Text("写真を選択して、メモを入力してください")
        OutlinedTextField(
            value = memo,
            onValueChange = { memo = it },
            modifier = Modifier.padding(20.dp),
            maxLines = 3,
        )
        Button(
            onClick = {
                // 保存処理
                save(context, bitmap!!, memo, imageUri.value!!)
                // ダイアログ表示
                showDialog = true
            }
        ){
            Text(text = "メモを保存する")
        }
    }
}

```

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/2b2c742c-4382-24c6-e26b-0174f6fe7ab6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/2b2c742c-4382-24c6-e26b-0174f6fe7ab6.png)

## 保存処理について

保存処理 `save` は次のような流れになります。

- ファイル名を生成
- 選択された画像をアップロード
- ファイル名とメモを紐付けてデータストアに保存

### ライブラリインポート

NCMBObjectとNCMBFileをインポートします。

```kotlin
// 記述してください
import com.nifcloud.mbaas.core.NCMBFile
import com.nifcloud.mbaas.core.NCMBObject
```

### ファイル名を生成

ファイルストアに保存するファイル名はユニークでないと上書き、またはエラーになってしまうのでランダムに生成するようにしています。

```kotlin
// 記述してください
// ファイル名を作成
val uuidString = UUID.randomUUID().toString()
val match = Regex("^.*\\\\.(.*)$").find(imageUri.toString())
val extension = if (match != null) match!!.groups[1]!!.value.lowercase() else "png"
val fileName = "${uuidString}.${extension}"
```

### 選択された画像をアップロード

作成したファイル名と、指定された写真のデータを使ってNCMBのファイルストアにアップロードを行います。この時、Fileオブジェクトで送る必要があるので、テンポラリファイルを作成しています。

```kotlin
// 記述してください
// NCMBファイルオブジェクトを作成
val file = NCMBFile(fileName = fileName, fileData = getFile(uuidString, extension, bitmap!!, context = context))
// ファイルアップロード
file.save()
```

`getFile` 関数は次の通りです。今回は画像フォーマットをJPEGまたはPNGとしています。JPEGの圧縮率を高めにしているのは、画像サイズが大きいとNCMBへアップロードエラーを起こすためです（フリープランは5MBまでになります）。

```kotlin
// 記述済み
// ファイルストア用にUriをテンポラリファイルに変換する
fun getFile(fileName: String, extension: String, bitmap: Bitmap, context: Context): File {
    // 出力先（テンポラリ）
    val outputDir = context.cacheDir
    val outputFile = File.createTempFile(fileName, ".${extension}", outputDir)
    // bitmapをByteArrayに変換する
    val stream = ByteArrayOutputStream()
    if (extension == "jpg" || extension == "jpeg") {
        bitmap.compress(Bitmap.CompressFormat.JPEG, 50, stream)
    } else if (extension == "png") {
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
    }
    // テンポラリファイルに出力
    outputFile.writeBytes(stream.toByteArray())
    return outputFile
}
```

### ファイル名とメモを紐付けてデータストアに保存

ファイルをアップロードしたら、そのファイル名をメモ内容と紐付けで保存します。

```kotlin
// 記述してください
// 次にメモを保存
val obj = NCMBObject("Memo")
// メモとアップロードしたファイル名を設定
obj.put("text", memo)
obj.put("fileName", fileName)
// 保存
obj.save()
```

これでファイルアップロードとメモデータの保存が完了です。 `save` 関数の内容は以下の通りです。

```kotlin
// 入力されたテキスト、画像データをNCMBに保存する関数
fun save(context: Context, bitmap: Bitmap, memo: String, imageUri: Uri) {
    val uuidString = UUID.randomUUID().toString()
    val match = Regex("^.*\\.(.*)$").find(imageUri.toString())
    val extension = if (match != null) match!!.groups[1]!!.value.lowercase() else "png"
    val fileName = "${uuidString}.${extension}"
    val file = NCMBFile(fileName = fileName, fileData = getFile(uuidString, extension, bitmap!!, context = context))
    file.save()
    // 次にメモを保存
    val obj = NCMBObject("Memo")
    obj.put("text", memo)
    obj.put("fileName", fileName)
    obj.save()
}
```

## 一覧画面について

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/06fc080e-4a2f-9ea6-0940-67841d67746e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/06fc080e-4a2f-9ea6-0940-67841d67746e.png)

データを表示するのは一覧画面（ListScreen）になります。まず画面だけ紹介します。

```kotlin
// 記述済み
@Composable
fun ListScreen() {
    // Memoクラスの検索結果が入る変数
    // ↓ エラーにならないためのダミー
    var ary = remember { mutableStateOf<List<Any>>(emptyList()) }
		// リストを読み込む部分(後述）
    LazyColumn(
    ) {
        // 一覧を出力
        items(ary.value) { obj ->
            ListRow(obj)
        }
    }
}

```

### aryの変更

`ary` の定義を以下のように変更してください。

```kotlin
// 変更前
var ary = remember { mutableStateOf<List<Any>>(emptyList()) }
// 変更後
var ary = remember { mutableStateOf<List<NCMBObject>>(emptyList()) }
```

### リストを読み込む

NCMBからデータを取り出す際にはNCMBQueryを使います。1つ目の引数は対象とするクラス名です。条件は色々付与できますが、今回は特に指定していません。`findInBackground` で非同期処理にてデータを取得します。

```kotlin
// Memoクラスを検索するクエリー
val query = NCMBQuery.forObject("Memo")
query.findInBackground(NCMBCallback { e, results ->
		if (e == null) {
				// 結果をaryに適用
				ary.value = results as List<NCMBObject>
		}
})
```

検索結果は remember で定義している ary に渡しています。その結果、LazyColumn を使って描画処理が行われます。実際の描画はListRowにて行います。

## ListRow Componentについて

ListRowは一覧画面の1行分のデータを表示する画面です。NCMBObjectを受け取りますので、そこにあるメモ（テキスト）と、ファイル名を使ってファイルダウンロードを行います。

```kotlin
// 記述済み
@Composable
fun ListRow(obj: Any) { // 本当はAnyではなくNCMBObject
    // サムネイル表示する画像
    var bitmap = remember { mutableStateOf<Bitmap?>(null) }
    val file = null　// ← ダミーです
		// ファイルをダウンロードする処理（後述）
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .fillMaxWidth()
            .padding(10.dp)
            .background(Color.White, RoundedCornerShape(5.dp))
    ) {
        // 画像データの有無で表示出し分け
        if (bitmap.value != null) {
            // 画像が指定されている場合
            Image(
                bitmap = bitmap.value!!.asImageBitmap(),
                contentDescription = null,
                modifier = Modifier
                    .size(150.dp)
                    .padding(10.dp)
            )
        } else {
            // 画像がない場合はアイコンを表示
            Icon(Icons.Rounded.Image, "説明", Modifier.size(size = 150.dp))
        }
        // メモを表示
        Text("")
    }
}

```

### ライブラリインポート

以下の3つのライブラリをインポートします。

```kotlin
import com.nifcloud.mbaas.core.NCMBCallback
import com.nifcloud.mbaas.core.NCMBFile
import com.nifcloud.mbaas.core.NCMBObject
```

### ListRowの受け取る変数の型を変更

Any型をNCMBObjectに変更します。これは一覧から送られてくる変数です。

```kotlin
// 変更前
fun ListRow(obj: Any) {

// 変更後
fun ListRow(obj: NCMBObject) {
```

### ファイルダウンロード処理について

ファイルダウンロードは、NCMBFileのfetchInBackgroundを使います。そしてデータはfileDownloadByteに入っていますので、それをBitmapに変換します。

```kotlin
// ファイルをダウンロードする処理（後述）
// ファイル名からNCMBFileのオブジェクトを作成
val file = NCMBFile(obj.getString("fileName")!!)
// ファイルダウンロード
file.fetchInBackground(NCMBCallback { e, data ->
		if (e == null && file.fileDownloadByte != null) {
				// ダウンロードした内容をbitmapに変換
				val data = file.fileDownloadByte!!
				bitmap.value = BitmapFactory.decodeByteArray(data, 0, data.size)
		}
})
```

これでダウンロード処理が完了です。画像をダウンロードしたら、一覧でサムネイル表示されます。

### メモ出力

メモの内容をテキスト出力します。

```kotlin
// メモを表示
Text(obj.getString("text")!!))
```

## まとめ

これでカメラメモアプリの完成です。データストアへのメモデータ保存とファイルストアへのファイルアップロード、そしてダウンロードを実装しました。NCMBには他にも位置情報検索やプッシュ通知など様々な機能があります。ぜひあなたのアプリ開発に活かしてください。

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