KotlinのNavigation コンポーネントを使ってNCMBObjectを別画面に渡す

Kotlin Jetpack Componentなどを使っていて、画面遷移を行う場合にはNavigation コンポーネントを使うと思います。このNavigation コンポーネントでは基本的に文字列や数字などを引数に指定できますが、オブジェクトをそのまま渡せないようです。

[デスティネーション間でデータを渡す  |  Android デベロッパー  |  Android Developers](https://developer.android.com/guide/navigation/navigation-pass-data?hl=ja)

マスター・詳細画面を作る場合には、NCMBObjectのリストをマスター画面に表示して、詳細画面には選択されたNCMBObjectを表示するというフローがよくあります。そうした時に、選択される度に都度ネットワークから取得し直すのは帯域もAPIコール数も勿体ないです。そこで一旦NCMBObjectを文字列にして、Navigation コンポーネントで戻してみました。

## 遷移元の処理

遷移元では、以下のようにして選択されたNCMBObject `obj` をJSONObjectにします。その上で `toString` を使ってJSON文字列としてナビゲーションに渡します。

```kotlin
val moveDetail = {
		val json = JSONObject()
		obj.keys.forEach {key ->
				json.put(key, obj.get(key)!!)
		}
		navController.navigate("detail/obj=${json.toString()}")
}
```

## ナビゲーションの処理

ナビゲーション側では、objを文字列として受け取ります。その文字列を後述する関数 `strToNCMBObject` に渡してNCMBObjectを受け取ります。遷移先の画面では普通に引数が受け取れるので、NCMBObjectを引き渡します。strToNCMBObjectの2つ目の引数はデータストアのクラス名です。

```kotlin
composable(
		route = "detail/obj={obj}",
		arguments = listOf(navArgument("obj") { type = NavType.StringType })
) { backStackEntry ->
		val obj = strToNCMBObject(backStackEntry.arguments!!.getString("obj")!!, "DailyReport")
		DetailScreen(navController, obj)
}
```

## strToNCMBObjectの処理

strToNCMBObjectは、基本的にJSONに入っているデータをNCMBObjectのキーとして適用するだけです。ただ、幾つかの規定のフィールドの場合は処理を分けています。

特に `updateDate` があると更新に失敗する（v1.1.0の場合。次の版では問題なくなるはずです）ので、省いています。

```kotlin
fun strToNCMBObject(str: String, className: String): NCMBObject {
		val json = JSONObject(str)
		val obj = NCMBObject(className)
		json.keys().forEach { key ->
				when (key) {
						"objectId" -> {
								obj.setObjectId(json.get(key) as String)
						}
						"createDate" -> {
								obj.setCreateDate(getDate(json.get(key) as String))
						}
						"updateDate" -> {} // obj.put("updateDate", getDate(json.get(key) as String))}
						"acl" -> {
								obj.setAcl(getAcl(json.get(key) as JSONObject))
						}
						else -> obj.put(key, json.get(key))
				}
		}
		return obj
}
```

### 日付の変換

日付はISO8601フォーマットなので、それに合わせて変換しています。

```kotlin
fun getDate(str: String): Date {
		val pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
		val format = SimpleDateFormat(pattern)
		format.timeZone = SimpleTimeZone(0, "UTC")
		return format.parse(str)
}
```

### ACLの変換

ACLの変換はぱっと見ではかなりややこしいです。これはACLが全員、ユーザー単位、グループ単位の3つに分かれているためです。この辺りも含め、将来のバージョンでは簡単に文字列とオブジェクトのやりとりができるようなる予定です。

```kotlin
fun getAcl(obj: JSONObject): NCMBAcl {
		val acl = NCMBAcl()
		obj.keys().forEach { key ->
				val map = obj.get(key) as JSONObject
				when {
						key == "*" -> {
								if (!map.isNull("read") && map.get("read") as Boolean) {
										acl.publicReadAccess = true
								}
								if (!map.isNull("write") && map.get("write") as Boolean) {
										acl.publicWriteAccess = true
								}
						}
						key.indexOf("role:") > -1 -> {
								val match = "role:(.*)$".toRegex().find(key)
								val roleName = match?.groups?.get(1)?.value
								if (roleName != null) {
										if (!map.isNull("read") && map.get("read") as Boolean) {
												acl.setRoleReadAccess(roleName, true)
										}
										if (!map.isNull("write") && map.get("write") as Boolean) {
												acl.setRoleWriteAccess(roleName, true)
										}
								}
						}
						else -> {
								if (!map.isNull("read") && map.get("read") as Boolean) {
										acl.setUserReadAccess(key, true)
								}
								if (!map.isNull("write") && map.get("write") as Boolean) {
										acl.setUserWriteAccess(key, true)
								}
						}
				}
		}
		return acl
}
```

## まとめ

文字列とNCMBObjectの相互変換ができれば、アプリ内で自由にデータが取り回せるようになります。画面遷移する際には必須になると思うので、ぜひご利用ください。

[ドキュメント : 開発者向けドキュメント | ニフクラ mobile backend](https://mbaas.nifcloud.com/doc/current/#/Kotlin)