【Android】Navigation ComposeのSafe Argsについて(フォルダの分け方など)


## はじめに
この記事はフラー株式会社 Advent Calendar 2024 12日目の記事です

11 日目は@masaya82さんの[UIコンポーネントをライブラリに頼ること](https://qiita.com/masaya82/items/ce2da853ef12529eb794)についてでした


## 1. なぜ記事を書いたか？
Navigation ComposeのSafeArgs周りでいい記事が見つからなかったから

## 2. 検証環境(2024/12/09 現在)
```
UIフレームワーク: Jetpack Compose
Kotlin 2.1.0
minSDK 24
Android Studio Ladybug | 2024.2.1
macOS Sonoma 14.7
```

## 3. ゴール
MVP(Minimum Viable Product)でNavigation ComposeのSafe Argsを体験(実装)してみる

## 4. 下準備(2024/12/04 現在)
- ライブラリをインポートする(Kotlinのバージョンを上げるなどは省略)
    - kotlin SerializationとNavigation Composeを入れる

```(Project) build.gradle
plugins {

    // Existing plugins
    alias(libs.plugins.compose.compiler) apply false

    // add: kotlin serialization
    kotlin("jvm") version "2.1.0"
    kotlin("plugin.serialization") version "2.1.0"
}
```


```(Module) build.gradle
plugins {

    alias(libs.plugins.compose.compiler)

    // add: kotlin serialization 
    id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0"
}

    
dependencies {

    // add: kotlin serialization
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")

    // add: navigation compose
    implementation("androidx.navigation:navigation-compose:2.8.4")
}
```

## 5. フォルダ構成
(今回 viewModelは実装していないが```〇〇Screen```の箇所に格納する想定)
```
- com.example.navArgsTest
  - navigation
      - MyAppNavHost.kt
      - Route.kt
  - screen
      - firstScreen
          - FirstScreen.kt
          - FirstScreenNavigation.kt
      - secondScreen
          - SecondScreen.kt
          - SecondScreenNavigation.kt
  - MainActivity.kt
  - MyApp.kt		
```
## 6. 処理の流れの概要
```mermaid
flowchart TB
  MainActivity.kt --> MA[MyApp.kt]
  MA --> MANH[MyAppNavHost.kt]
  MANH --> R[Route.kt]
  R --> FSN[FirstScreenNavigation.kt] & SSN[SecondScreenNavigation.kt]
  FSN --> fs[FirstScreen.kt]
  SSN --> sS[SecondScreen.kt]
```

## 7. MainActivity
MainActivityはMyAppを参照するように実装
``` MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            enableEdgeToEdge()
            ComposeNavigationSafeArgsTestTheme {
                MyApp()
            }
        }
    }
}
```
## 8. MyApp
MyAppにnavControllerを定義
MyAppNavHostにnavControllerを渡すように実装

``` MyApp.kt
@Composable
fun MyApp() {
    val navController = rememberNavController()
    MyAppNavHost(navController)
}
```

## 9. MyAppNavHost
画面の格納をしているイメージ
NavHostの引数はnavControllerとstartDestination(どの画面が最初に来るか？)を渡す

```firstScreen()```と```secondScreen()```はNavGraphBuilderの拡張関数
リファクタリングをしてこのような形としている

``` MyAppNavHost.kt
@Composable
fun MyAppNavHost(
    navController: NavHostController = rememberNavController()
) {
    NavHost(
        navController = navController,
        startDestination = Route.FirstScreenRoute,
    ) {
    
        // firstScreen
        firstScreen(navController)

        // secondScreen
        secondScreen(navController)
    }
}
```


## 10. Route
Routeを定義することによってタイポを減らす目的がある.

``` Route.kt
object Route {
    @Serializable
    object FirstScreenRoute

    @Serializable
    data class SecondScreenRoute(
        val name: String,
        val id: String
    )
}
```


## 11. 画面の定義

### First Screen
``` FirstScreen.kt
@Composable
fun FirstScreen(navController: NavController) {
    Box(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxSize(), 
        contentAlignment = Alignment.Center
    ) {
        Column {
            Text(
                "This is First Screen",
                modifier = Modifier.padding(8.dp),
                fontWeight = FontWeight.Bold
            )
            Button(
                onClick = {
                    navController.navigateToSecondScreen(name = "John", id = "012345FF")
                },
            ) { Text("Go To Second Screen") }
        }
    }
}
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989129/bf5a4f56-34d1-d9ae-d6a3-1a8cc9e86f59.png" width=50%>

### Second Screen
``` SecondScreen.kt
@Composable
fun SecondScreen(name: String, id: String, navController: NavController) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "This is Second Screen", fontWeight = FontWeight.Bold)
        Text(text = "name: $name")
        Text(text = "id: $id")
        Button(onClick = { navController.navigateToFirstScreen() }) {
            Text(text = "Go To First Screen")
        }
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989129/ccfc91f9-2eb8-b6ac-3d6a-fe461efb9a72.png" width=50%>


## 12. Navigation定義
### ポイント
1. Navigationをするにあたってnavigate先をJsonSerializableオブジェクトを指定する(今回の場合 Route.〇〇Route)
1. 値を渡すときはJsonSerializableのdata classを定義しそこから値を渡す(SecondScreenNavigation参照)
1. ```backStackEntry.toRoute()```でそれぞれの値を取得して画面遷移時に渡す(SecondScreenNavigation参照)

``` FirstScreenNavigation.kt

fun NavController.navigateToFirstScreen() {
    navigate(Route.FirstScreenRoute)
}

fun NavGraphBuilder.firstScreen(navController: NavController) {
    composable<Route.FirstScreenRoute> {
        FirstScreen(navController)
    }
}
```

``` SecondScreenNavigation.kt

// navControllerの拡張関数(navigateのラッパーみたいなイメージ)
fun NavController.navigateToSecondScreen(name: String, id: String) {
    navigate(Route.SecondScreenRoute(name = name, id = id))
}

// navGraphBuilderの拡張関数(画面定義のラッパーみたいなイメージ)
fun NavGraphBuilder.secondScreen(navController: NavController) {
    composable<Route.SecondScreenRoute> { backStackEntry ->
        val secondScreenRouteArgs: Route.SecondScreenRoute = backStackEntry.toRoute()
        SecondScreen(secondScreenRouteArgs.name, secondScreenRouteArgs.id, navController)
    }
}
```


## 13. 参考文献
- [Navigation with Compose](https://developer.android.com/develop/ui/compose/navigation?hl=ja)
- [Design your navigation graph](https://developer.android.com/guide/navigation/design#compose)
- [Navigate to a destination](https://developer.android.com/guide/navigation/use-graph/navigate)
- [Type safety in Kotlin DSL and Navigation Compose](https://developer.android.com/guide/navigation/design/type-safety#type-safe)
- [Encapsulate your navigation code](https://developer.android.com/guide/navigation/design/encapsulate)
- [Nested graphs](https://developer.android.com/guide/navigation/design/nested-graphs)
- [Fragments and the Kotlin DSL](https://developer.android.com/guide/navigation/design/kotlin-dsl)
- [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization)
- [nowinandroid](https://github.com/android/nowinandroid)