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()}
}
}
}
}
}
なお、アイコンが不足しているので 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 = "メモを保存する")
}
}
}
保存処理について
保存処理 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のファイルストアへのファイルアップロード、そしてデータストアへのデータ保存が完了しました。次回は保存したデータを一覧表示する部分を開発します。