JetpackComposeのNavigation Componentを触ったのでまとめる

主に[公式ドキュメント](https://developer.android.com/jetpack/compose/navigation)に書いてることを自分なりに解釈した言葉でまとめます。

Composeのプロジェクトを作ったら `MainActivity`の `onCreate（）`に `setContent`が配置されてると思いますが、その中で `NavHost`を用意してあげます。ここが従来のNavigation ComponentのNavGraphみたいな感じになります。

# まずは普通に画面遷移
`NavHost`を用いて画面遷移する際に `NavController`のインスタンスが必要なので、 `NavHost`をつくる前に作っておきます。

```kotlin
val navController = rememberNavController()
```

次に `NavHost`を定義します。 `setContent`の中で呼ばれるように `@Composable`なメソッドの中で定義します。

```kotlin
    setContent {
        NavHost(navController = navController, startDestination = "home") {
            composable(route = "home") {
                HomeScreen()
            }
            composable(route = "detail") {
                DetailScreen()
            }
        }   
    }
```

よくありそうな、Home画面のようなリストがある画面から1つのItemをタップしたときに詳細画面に遷移するイメージの `NavHost`です。
`NavHost`の第2引数には、従来のNavigation Componentにも必要だった `startDestination`を設定します。そうすることで、このKeyが `route`に設定されている画面から起動します。

`HomeScreen()`では、リストの中のItemがタップされたときに `DetailScreen()`に遷移したいのですが、この際に、最初に定義した `navController`を `HomeScreen()`に渡してあげる必要があります。

`HomeScreen()`の中で実際になにかタップされたときに `DetailScreen()`に遷移したいときはこんな感じです。

```kotlin
@Composable
fun HomeScreen(navController: NavController) {
    Button(
        onClick = {
            navController.navigate("detail")
        }
    ) {
    }
}
```

`DetailScreen()`に遷移後、 `Toolbar`などに置かれた戻るボタンが押されたときに前の画面に戻りたいときはこうです。

```kotlin
navController.popBackStack()
```

# 遷移先に値を渡す
`HomeScreen()`から `DetailScreen()`に遷移したときに何かしら値を渡したいときがよくあると思います。
まずは `String`を渡せるようにしてみましょう。

```kotlin
    setContent {
        NavHost(navController = navController, startDestination = "home") {
            composable(route = "home") {
                HomeScreen(navController = navController)
            }
            composable(
                route = "detail/{id}",
                arguments = listOf(navArgument("id") { type = NavType.StringType })
            ) { backStackEntry ->
                val argId = backStackEntry.arguments?.getString("id") ?: return@composable
                DetailScreen(id = argId)
            }
        }
    }
```

この `NavHost`では `HomeScreen()`から `DetailScreen()`に `id`という名前の `String`を遷移時に渡しています。
`HomeScreen()`で遷移するときは、こんな感じです。

```kotlin
val id = "hogeId"
navController.navigate("detail/$id")
```

`NavHost`を定義するときに、 `backStackEntry`を用いて遷移元から渡ってきた値を取り出すことができます。
また、 `arguments`で定義している `NavType`には他にもたくさんあります。
![スクリーンショット 2021-04-18 16.10.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185927/76acc288-8efb-a18c-fc8f-90f717449c8e.png)

`String`の渡し方はわかりましたが、実際に開発していて `String`や `Boolean`などの簡単な型の値を渡したくなるときはあまりなく、 `User`がたくさん並んでるリストから1つタップして `User`型を `UserDetailScreen()`に渡して表示するみたいなユースケースが多いと思います。
そこで、 `NavType`には `NavType.SerializableType`というのがいるので、 `Serializable`な `data class`を定義してそれを渡すようにすれば良さそうです。

```kotlin
data class User(
    val name: String,
) : Serializable
```

コードの全体像はこんな感じです。

```kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navController = rememberNavController()
            NavHost(navController = navController, startDestination = "home") {
                composable(route = "home") {
                    HomeScreen(navController = navController)
                }
                composable(
                    route = "detail/{user}",
                    arguments = listOf(navArgument("user") { type = NavType.SerializableType(User::class.java) })
                ) { backStackEntry ->
                    val user = backStackEntry.arguments?.getSerializable("user") as User
                    DetailScreen(navController = navController, user = user)
                }
            }
        }
    }
}

@Composable
fun HomeScreen(navController: NavController) {
    Scaffold {
        Column {
            Text(text = "This is HomeScreen")
            val user = User(name = "hoge")
            Button(onClick = { navController.navigate("detail/$user") }) {
            }
        }
    }
}

@Composable
fun DetailScreen(navController: NavController, user: User) {
    Scaffold {
        Column {
            Text(text = "This is DetailScreen $user")
            Button(onClick = { navController.popBackStack() }) {
            }
        }
    }
}
```

これを実行して、実際に遷移させると以下のようなエラーが起きてうまく動きません。

```
java.lang.UnsupportedOperationException: Serializables don't support default values.
```

色々と調べてみましたが、わたしの方ではこの解決策は見つけることができませんでした…。
もし、解決策をご存知の方がいましたらご教授願います。。。

そこで、こんな動画を見つけました。
<iframe width="560" height="315" src="https://www.youtube.com/embed/OgYfQNbl0ts" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

この動画では、これに対処するために `User`を一度 `Gson`を用いてJsonに変換したものを `String`として遷移先に渡して、遷移先の今回でいう `DetailScreen()`で `String`を `User`に変換して取り出す方法で実装していました。

一応これで自分で定義した `data class`を遷移先に渡すことができましたが、今はまだこのように実装するしかないのでしょうか…

# 遷移後に前の画面に戻れないようにする
今までのユースケースでは遷移後に戻るボタンで前の画面に戻れてよかったのですが、ログイン画面でログインボタンを押してホーム画面に戻ってくるときなど、遷移後に遷移前の画面に戻ってほしくないときがあると思います。
そのときは、遷移時にこのように実装します。

```kotlin
            Button(
                onClick = {
                    navController.navigate("home") {
                        popUpTo("login") { inclusive = true }
                    }
                }
            )
```

ホームに `navigate`するときに同時に `inclusive`というのを設定して現在のログイン画面を `popUpTo`してあげます。
これによってAndroidの戻るボタンを押しても前の画面に戻らないような挙動を実現できます。

# ネストしたNavigation
従来のNested navigation graphsのように、ComposeのNavigation Componentでもアプリ内の特定の画面遷移をモジュール化することができます。
たとえば、ログインやユーザ登録などの認証機能周りの画面遷移のモジュールと、ログイン後のBottomNavigationがあるような様々な機能に触れる画面遷移のモジュールなどでしょうか。

`NavHost`の中で `navigation`を用いて特定の画面遷移の塊をモジュール化してこのように実装します。

```kotlin
        setContent {
            val navController = rememberNavController()
            NavHost(navController = navController, startDestination = "home") {
                navigation(startDestination = "login", route = "auth") {
                    composable(route = "login") {
                        LoginScreen()
                    }
                    composable(route = "register") {
                        RegisterScreen()
                    }
                }

                composable(route = "home") {
                    HomeScreen(navController = navController)
                }
                composable(
                    route = "detail/{user}",
                    arguments = listOf(navArgument("user") { type = NavType.SerializableType(User::class.java) })
                ) { backStackEntry ->
                    val user = backStackEntry.arguments?.getSerializable("user") as User
                    DetailScreen(navController = navController, user = user)
                }
            }
        }
```

認証周りの画面遷移を `auth`という名前で定義して、遷移時に以下のようにすると `auth`のNavigationの塊を見て `startDestination`で定義されている `login`に遷移します。

```kotlin
navController.navigate("auth")
```

# おわり
ほかにも、ComposeのNavigation Componentにはディープリンクの機能もあってintent filterで特定のComposeの画面に遷移させることもできそうです。

Composeはまだbetaで、私も探り探りなので色々な人と知見を共有して他にもこんなふうに書けるよや、そこは使い方間違ってるよなどあれば是非コメントください。

# 参考
- https://developer.android.com/jetpack/compose/navigation
- https://youtu.be/OgYfQNbl0ts