Jetpack Compose ベースの Navigation まとめ

## はじめに

- 以前 Fragment ベースの Navigation について書いたので、 Compose ベースでの Navigation をどう実装するか調べてみました
- ごく基本的なところをまとめた内容なので、トラブルシューティングや応用については参考にならないと思います（ごめんなさい）
- メモ書きをそのまま起こしただけなので、文章が雑多な点はご承知おきください

## インストール

下記はあくまで Navigation に関連する依存関係だけなので、 Compose 関連のものは別途追加すること

```kotlin
implementation("androidx.navigation:navigation-compose:$navigation_version")
```

## 基本的な使い方

大まかな手順は以下の通り

1. `NavController` を取得
2. 下記いずれかの方法で Navigation Graph を定義
    - `NavHost` の builder パラメータに渡すラムダで定義
    - `NavController` から `NavGraph` を生成し、それを `NavHost` に与える
3. [NavController.navigate](https://developer.android.com/reference/kotlin/androidx/navigation/NavController#navigate(kotlin.String,kotlin.Function1)) を呼んで画面遷移を発生させる

## NavController の取得

`rememberNavController` コンポーザブルで取得

[rememberNavController](https://developer.android.com/reference/kotlin/androidx/navigation/compose/package-summary#rememberNavController(kotlin.Array))

```kotlin
@Composable
fun rememberNavController(vararg navigators: Navigator<NavDestination>): NavHostController
```

## Navigation Graph の定義

### NavHost コンポーザブルで定義する場合

[NavHost](https://developer.android.com/reference/kotlin/androidx/navigation/compose/package-summary#NavHost(androidx.navigation.NavHostController,kotlin.Any,androidx.compose.ui.Modifier,androidx.compose.ui.Alignment,kotlin.reflect.KClass,kotlin.collections.Map,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function1)) の builder パラメータに渡すラムダで定義

```kotlin
@Composable
fun NavHost(
    navController: NavHostController,
    startDestination: Any,
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    route: KClass<*>? = null,
    typeMap: Map<KType, NavType<*>> = emptyMap(),
    enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
            fadeIn(animationSpec = tween(700))
        },
    exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
            fadeOut(animationSpec = tween(700))
        },
    popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = enterTransition,
    popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = exitTransition,
    sizeTransform: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> SizeTransform)? = null,
    builder: NavGraphBuilder.() -> Unit
): Unit
```

- `builder` に渡すラムダの中で Navigation Graph を定義する
- 最終的に `NavController.createGraph` が呼び出されて Navigation Graph が生成されるとのこと
    - 要はどちらにせよ、次項「NavController の拡張関数 createGraph で定義する場合」の内容と同じことが行われる

### NavController の拡張関数 createGraph で定義する場合

まず [NavController.createGraph](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#(androidx.navigation.NavController).createGraph(kotlin.String,kotlin.String,kotlin.Function1)) で Navigation Graph を定義

```kotlin
inline fun NavController.createGraph(
    startDestination: String,
    route: String? = null,
    builder: NavGraphBuilder.() -> Unit
): NavGraph
```

次に `NavController.createGraph` の戻り値を [NavHost](https://developer.android.com/reference/kotlin/androidx/navigation/compose/package-summary#NavHost(androidx.navigation.NavHostController,kotlin.Any,androidx.compose.ui.Modifier,androidx.compose.ui.Alignment,kotlin.reflect.KClass,kotlin.collections.Map,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function1)) の `graph` パラメータに渡す

```
@Composable
fun NavHost(
    navController: NavHostController,
    graph: NavGraph,
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
            fadeIn(animationSpec = tween(700))
        },
    exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
            fadeOut(animationSpec = tween(700))
        },
    popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = enterTransition,
    popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = exitTransition,
    sizeTransform: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> SizeTransform)? = null
): Unit
```

## サンプルコード (Navigation Graph の定義)

### NavHost で Navigation Graph を定義する場合

```kotlin
NavHost(
    navController = navController,
    startDestination = "start_screen",
    modifier = modifier,
) {
    composable(route = "start_screen") {
        StartComposable()
    }
    composable(route = "route1") {
        SomeComposable1()
    }
    composable(route = "route2") {
        SomeComposable2()
    }
    ...
}
```

### NavController.createGraph で Navigation Graph を定義してから NavHost に与える場合

```kotlin
val graph = navController.createGraph(
    startDestination = "start_screen",
) {
    composable(route = "start_screen") {
        StartComposable()
    }
    composable(route = "route1") {
        SomeComposable1()
    }
    composable(route = "route2") {
        SomeComposable2()
    }
    ...
}

NavHost(
    navController = navController,
    graph = graph,
    modifier = modifier,
)
```

## デスティネーションと、そこへ到達するためのルートを定義

`NavHost` や `NavController.createGraph` の `builder` に与えるラムダで `NavGraphBuilder.composable` を呼び出して、個々のデスティネーションとルートを定義する

### NavGraphBuilder.composable

1回の `composable` 呼び出しごとに1つのデスティネーション＆ルートを定義する

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/navigation/compose/package-summary#(androidx.navigation.NavGraphBuilder).composable(kotlin.String,kotlin.collections.List,kotlin.collections.List,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function1,kotlin.Function2)) （オーバーロードあり）

```kotlin
fun NavGraphBuilder.composable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? = null,
    exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? = null,
    popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? = enterTransition,
    popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? = exitTransition,
    sizeTransform: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> SizeTransform)? = null,
    content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
): Unit
```

- 遷移先を描画するコンポーザブルの呼び出しは `content` で行う
- `route` にセットした文字列を引数にして `NavController.navigate` が実行されると `content` が呼び出される。この中で遷移先のデスティネーションをコンポーズする
    - `route` として `navigate` に渡せるのは文字列だけではないが、ここでは割愛
- 遷移元から遷移先へ引数を渡す場合は以下の実装が必要
    - `route` には `"route_name/{args_name}"` というように、『ルート名＋引数名を {} で囲んだ文字列』を指定
    - `arguments` には `navArgument` 関数（下記）を利用して、引数名と引数のデータ型をリストで定義
- 実際に遷移先に渡される引数は、 `content` の中で `NavBackStackEntry.arguments` を介して受け取ることができる

## 画面遷移時に引数を渡す

引数の渡し方は他にもあるが、ここではルートに `"route_name/{args_name}"` を渡すことでやり取りする方法についてのみ記載

### navArgument 関数

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#navArgument(kotlin.String,kotlin.Function1))

```kotlin
fun navArgument(name: String, builder: NavArgumentBuilder.() -> Unit): NamedNavArgument
```

- この関数で生成される `NamedNavArgument` のリストを `composable` 関数の `arguments` パラメータに渡す
- 1つの引数について、引数名とその型を定義する

### NavBackStackEntry 関数

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/navigation/NavBackStackEntry)

```kotlin
class NavBackStackEntry : LifecycleOwner, ViewModelStoreOwner, HasDefaultViewModelProviderFactory
```

- `composable` 関数の `content` ラムダに渡ってくる
- 遷移先に渡す引数は [NavBackStackEntry.arguments](https://developer.android.com/reference/kotlin/androidx/navigation/NavBackStackEntry#arguments()) を介して取得する

## ディープリンクの定義

### navDeepLink 関数

[APIリファレンス](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#navDeepLink(kotlin.Function1)) （オーバーロードあり）

```kotlin
fun navDeepLink(deepLinkBuilder: NavDeepLinkDslBuilder.() -> Unit): NavDeepLink
```

- この関数で生成される `NavDeepLink` のリストを、 `composable` 関数の `deeplinks` パラメータに渡す
- デスティネーションに遷移させるためのディープリンクを1個定義する
- 引数をつける場合、 `deepLinkBuilder` で呼び出す `uriPattern` には『スキーマ＋ホスト名＋引数名を {} で囲んだ文字列』を渡す
    -  `composable` 関数の `route` パラメータに渡す文字列と同じルール
    - スキーマを “sample” に、デスティネーションを “destination1” に、引数名を “param_name” とするなら `sample://destination1/{param_name}` となる

## サンプルコード (様々な種類のルートを定義)

```kotlin
// NavController を取得
val navController = rememberNavController()

// NavHost を設置
// - startDestination には起点となるルートを指定
// - 後置ラムダが NavGraph で、 composable 関数に route と遷移先とする Composable の呼び出しを定義する
NavHost(
    navController = navController,
    startDestination = "first_page",
    modifier = Modifier,
) {
    // 引数を取らない場合
    // シンプルにルートに対応する Composable を呼び出す
    composable(route = "first_page") {
        FirstPageComposable()
    }
    // 引数を取る場合
    // ルートは "route_name/{args_name}" というように「ルート名＋引数名を{}で囲んだ文字列」で定義する
    // 引数とその型は navArguments 関数で定義したものを、 arguments パラメータにリストで与える
    // - 第1引数に引数名を、第2引数にラムダで引数の型をセット
    // 引数を取るディープリンクは navDeepLinks 関数で定義したものを deeplinks パラメータにリストで与える
    composable(
        route = "second_page/{param_name}",
        arguments = listOf(
            navArgument("param_name") { type = NavType.StringType }
        ),
        deepLinks = listOf(
            navDeepLink { uriPattern = "sample://destination1/{param_name}" },
        ),
    ) { navBackStackEntry ->
        // Nav引数の取得
        val args = navBackStackEntry.arguments?.getString("param_name")
        
        ThirdPageComposable(arguments = args)
    }
}
```

## おわりに

- `Fragment` ベースの Navigation で Fragment to Fragment の遷移でしたが、 Compose ベースの Navigation は、画面の再描画に近いイメージを持ちました
    - 従来の画面遷移と同じ感覚で使おうとしたら痛い目を見そうな気がします
    - Compose 全般に言えることかも知れませんが、その気になれば1つの Activity に全ての画面遷移を書けなくもないので、 Fragment の使い分けをどうしていくか考える必要があるなという感想です
- Serializable なオブジェクトを使って、型安全性を担保しつつデスティネーション間で値をやり取りする方法もあるようですが、使用できる Navigation のバージョンが限られており、後日改めてキャッチアップすべきかと思いました