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/5c0178e691dccdd42ade)ので、今回はログインとデータの保存について解説します。

## 完成版のコード

作成したデモアプリのコードは[NCMBMania/Kotlin_DailyReport](https://github.com/NCMBMania/Kotlin_DailyReport)にアップロードしてあります。

## ナビゲーションの振り分け

今回は認証前提とし、認証していない場合はLoginScreen、認証している場合はListScreenを表示します。ナビゲーションでは、認証状態を判定し、それに合わせて画面の出し分けをします。

```kotlin
fun Navigation() {
    var startDestination by remember { mutableStateOf("list") }
    val changeLoginStatus = {
        val currentUser: NCMBUser = NCMBUser().getCurrentUser()
        startDestination = if (currentUser.getObjectId() == null) "login" else "list"
        Unit
    }
    changeLoginStatus()
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = startDestination) {
			// 省略
		}
}
```

### ナビゲーションの設定

ナビゲーションのルーティング設定は次の通りです。一覧から詳細、詳細から編集へ遷移する際には対象データ `NCMBObject` を文字列で受け取り、それをオブジェクトに復元した上で画面遷移しています。 `strToNCMBObject` 関数については[KotlinのNavigation コンポーネントを使ってNCMBObjectを別画面に渡す - Qiita](https://qiita.com/goofmint/items/a0cd211fbcaafb375352)にて解説しています。

```kotlin
val navController = rememberNavController()
NavHost(navController = navController, startDestination = startDestination) {
		// ログイン画面
		composable(route = "login") {
				LoginScreen(navController, changeLoginStatus)
		}
		// 新規作成画面
		composable(route = "form") {
				val obj = NCMBObject("DailyReport")
				FormScreen(navController, obj)
		}
		// 一覧画面
		composable(route = "list") {
				ListScreen(navController = navController)
		}
		// 詳細画面
		composable(
				route = "detail/obj={obj}",
				arguments = listOf(navArgument("obj") { type = NavType.StringType })
		) { backStackEntry ->
				val obj = strToNCMBObject(backStackEntry.arguments!!.getString("obj")!!, "DailyReport")
				DetailScreen(navController, obj)
		}
		// 編集画面
		composable(
				route = "edit/obj={obj}",
				arguments = listOf(navArgument("obj") { type = NavType.StringType })
		) { backStackEntry ->
				val obj = strToNCMBObject(backStackEntry.arguments!!.getString("obj")!!, "DailyReport")
				FormScreen(navController, obj)
		}
}
```

## ログイン画面

ログイン画面は `LoginScreen` として定義しています。

```kotlin
@Composable
fun LoginScreen(navController: NavController, callback: () -> Unit) {
		// ユーザー名
    var userName by remember { mutableStateOf("") }
		// パスワード
    var password by remember { mutableStateOf("") }
		// 処理中のフラグ
    var progress by remember { mutableStateOf(false) }
		// ダイアログ表示のフラグ
    var showDialog by remember { mutableStateOf(false) }
		// ログイン処理用の関数
    val login = {
				// 後述
    }
		// ダイアログ表示用
    if (showDialog) {
        AlertDialog(
            onDismissRequest = {},
            buttons = {
                Button(onClick = {
                    showDialog = false
                }) {
                    Text("OK")
                }
            },
            title = {Text("認証エラー")},
            text = {Text("認証できませんでした（${userName}）")}
        )
    }
		// 以下は画面構築
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        OutlinedTextField(
            value = userName,
            onValueChange = { userName = it },
            modifier = Modifier.padding(20.dp),
            label = { Text("ユーザ名") },
        )
        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            modifier = Modifier.padding(20.dp),
            visualTransformation = PasswordVisualTransformation(),
            label = { Text("パスワード") },
        )
        Button(onClick = login,
            enabled = !progress,
            modifier = Modifier.padding(20.dp)
        ) {
            if (progress) {
                CircularProgressIndicator(
                    strokeWidth = 2.dp,
                    modifier = Modifier.size(24.dp)
                )
            } else {
                Text("ログイン")
            }
        }
    }
}
```

![Screenshot_20220414_172403.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/6c34bf46-a0d1-46dd-8a64-841c824c95b8.png)


### ログイン処理について

progressをtrueにすると、ボタンが無効化されて多重実行を防げます。ログイン処理はNCMBUserに対してユーザー名とパスワードを設定し、`login` メソッドを実行します。

認証が完了したら、`callback` 関数を呼び出しています。これはナビゲーションで設定した `changeLoginStatus` です。これで認証後、一覧画面の表示に切り替わります。

```kotlin
val login = {
		try {
				progress = true
				val user = NCMBUser()
				user.userName = userName
				user.password = password
				user.login()
				callback()
		} catch(e: NCMBException){
				showDialog = true
		}
		progress = false
}
```

## データの新規保存

データを入力するのは `FormScreen` になります。FormScreenは新規作成と編集を兼ねているので、受け取った `obj` の状態によってメッセージやデフォルト値を分けています。

```kotlin
@Composable
fun FormScreen(navController: NavController, obj: NCMBObject) {
		// 新規作成と編集でデフォルト値を変更
		val objectId = obj.getObjectId()
    var title by remember { mutableStateOf(if (objectId == null) "" else obj.getString("title")!!) }
    var body by remember { mutableStateOf(if (objectId == null) "" else obj.getString("body")!!) }
		// 画面のヘッダー用
		val header = if (objectId == null) "新規作成" else "編集"
		// 処理中のフラグ
    var progress by remember { mutableStateOf(false) }
		// ダイアログ表示のフラグ
    var showDialog by remember { mutableStateOf(false) }
		// ダイアログメッセージ
    var message by remember { mutableStateOf("") }
		// ダイアログ表示
    if (showDialog) {
        AlertDialog(
            onDismissRequest = {},
            buttons = {
                Button(onClick = {
                    showDialog = false
                }) {
                    Text("OK")
                }
            },
            title = {Text("日報保存")},
            text = {Text(message)}
        )
    }
    val save = {
			// 保存処理（後述）
    }
		// 以下画面構築
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text("日報の${header}")
                },
                actions = {
                    IconButton(
                        onClick = {
                        }
                    ){
                        Icon(Icons.Filled.Save, contentDescription = "Save")
                    }
                }
            )
        },
        content = {
            Column {
                OutlinedTextField(
                    value = title,
                    onValueChange = { title = it },
                    modifier = Modifier.padding(20.dp),
                    label = { Text("日報のタイトル") },
                    singleLine = true
                )
                OutlinedTextField(
                    value = body,
                    onValueChange = { body = it },
                    modifier = Modifier.padding(20.dp).height(150.dp),
                    label = { Text("日報の本文") },
                )
                Button(onClick = save,
                    enabled = !progress,
                    modifier = Modifier.padding(20.dp)
                ) {
                    if (progress) {
                        CircularProgressIndicator(
                            strokeWidth = 2.dp,
                            modifier = Modifier.size(24.dp)
                        )
                    } else {
                        Text("保存する")
                    }
                }
            }
        }
    )
}
```

![Screenshot_20220414_172211.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/8bad2919-3ed3-8910-0da6-7e3cfb410b65.png)


### データの保存処理について

データの保存は、まず入力された内容を `put` メソッドでNCMBObjectに関連付けします。そして、NCMBAclを使ってアクセス権限を設定します。ログインしたユーザー情報は `NCMBUser().getCurrentUser()` で取得できます。そのユーザーに対してのみ、書き込み権限を付与しています（読み込みは誰でも可能としています）。そして `save` メソッドで保存完了です。

```kotlin
val save = {
	// 保存処理（後述）
		progress = true
		try {
				obj.put("title", title)
				obj.put("body", body)
				// アクセス権限の設定
				val acl = NCMBAcl()
				val currentUser = NCMBUser().getCurrentUser()
				acl.publicReadAccess = true
				acl.setUserWriteAccess(currentUser.getObjectId()!!, true)
				obj.setAcl(acl)
				// 保存
				obj.save()
				title = ""
				body = ""
				message = "日報を保存しました"
		} catch(e: NCMBException){
				message = "日報が保存できませんでした"
		}
		showDialog = true
		progress = false
}
```

## まとめ

NCMBを使えば認証やデータの保存が簡単に実現できます。ぜひあなたのアプリ開発に利用してください。

今回はログイン処理とデータの保存（同じコードで編集も行えます）を行いました。次回はデータの検索と表示について解説します。