NCMB Kotlin SDKを使ってカメラメモアプリを作る（その2 ファイルアップロードとデータ保存）

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を使ってカメラメモアプリを作ってみます。[前回は画面の仕様とSDKの初期化について解説しました](https://qiita.com/goofmint/items/59f4c23d3245aae1388d)ので、今回はデータを保存する処理について解説します。

## 完成版のコード

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

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

ナビゲーションバーのコードは以下のようになります。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()}
                }
            }
        }
    }
}
```

![Screenshot_1649852241のコピー.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 = "メモを保存する")
        }
    }
}
```

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


## 保存処理について

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

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

### ファイル名を生成

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

```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()
```

これでファイルアップロードとメモデータの保存が完了です。

## まとめ

今回までの処理でNCMBのファイルストアへのファイルアップロード、そしてデータストアへのデータ保存が完了しました。[次回は保存したデータを一覧表示する部分を開発します](https://qiita.com/goofmint/items/2d9c655049662796ca49)。

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