0
Help us understand the problem. What are the problem?

posted at

updated at

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の初期化について解説しましたので、今回はデータを保存する処理について解説します。

完成版のコード

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

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

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

// タブ用のクラス
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

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

implementation "androidx.compose.material:material-icons-extended:$compose_version"

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

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 関数が呼ばれることです。他は画像が選ばれると、デフォルトのアイコン表示と差し替わるのと、アラート表示を用意していることくらいでしょう。

@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

保存処理について

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

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

ファイル名を生成

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

// ファイル名を作成
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オブジェクトで送る必要があるので、テンポラリファイルを作成しています。

// NCMBファイルオブジェクトを作成
val file = NCMBFile(fileName = fileName, fileData = getFile(uuidString, extension, bitmap!!, context = context))
// ファイルアップロード
file.save()

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

// ファイルストア用に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
}

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

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

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

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

まとめ

今回までの処理でNCMBのファイルストアへのファイルアップロード、そしてデータストアへのデータ保存が完了しました。次回は保存したデータを一覧表示する部分を開発します

mBaaSでサーバー開発不要! | ニフクラ mobile backend

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
0
Help us understand the problem. What are the problem?