Compose Multiplatformで利用できるようになったNavigationを使ってみた

## はじめに

[Compose Multiplatform](https://www.jetbrains.com/ja-jp/lp/compose-multiplatform/)は、[Jetpack Compose](https://developer.android.com/develop/ui/compose/) をベースにKMP(Kotlin Multiplatform)プロジェクトでUIを構築するためのフレームワークです。
ここ最近は、目まぐるしいスピードで進化をしており、Resource周りも共通で実装できるようになっています！
そして、alpha版ではありますが、ついに `Navigation` も利用できるようになりました🎉

実際に `Navigation` を利用したところ体験が良かったため、本記事で紹介します！

## Navigationとは？

[Navigation](https://developer.android.com/guide/navigation)は、アプリ内の様々なコンテンツ間を遷移するためのAndroidでのライブラリです。
このライブラリを利用することで、今まで複雑でした画面遷移の処理を簡潔にしてくれます。
もちろん、Jetpack Compose でも利用でき、 [navigation-compose](https://developer.android.com/develop/ui/compose/navigation) として提供されています。

## Compose MultiplatformにおけるNavigationは？

Compose Multiplatformでは、UIのみの共通化とshared/commonMain/App.kt。
そのため、Navigationに関しては、各々のプラットフォームで行う必要があります。

Android, iOSのみのマルチプラットフォームプロジェクトと仮定して、以下に実装例を書きます。

### KMP

UIの実装をKMP側で持たせて共通化させます。

``` shared/commonMain/HomeContent.kt
@Composable
fun HomeContent(
  state: HomeState
) {
  Sample(state = state) // stateを表示するコンポーネント
}
```

``` shared/commonMain/SettingContent.kt
@Composable
fun SettingContent(
  modifier: Modifier = Modifier
) {
  LazyColumn(
    modifier = modifier
  ) {
    SettingItem.entries.forEach {
      // SettingItemで定義されている値をもとに項目のコンポーネントを作成する
    }
  }
}
```

### Android

先程記述したように、`navigation-compose` をプラットフォーム側で実装し、そこに　`Compose　Multiplatform` で実装した共通のUI呼び出す形で実装してました。

``` androidApp/MainActivity.kt
class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    setContent {
      BaseTheme {
        AppNavHost()
      }
    }
  }
}
```

``` androidApp/AppNavHost.kt
@Composable
fun AppNavHost(
  navController: NavHostController = rememberNavController(),
) {
  NavHost(
    navController = appScaffoldState.navController,
    startDestination = HomeNavGraph.home
  ) {
    homeNavGraph(
      navigateSetting = {
        navController.navigate(SettingNavGraph.setting)
      }
    )
    settingNavGraph(
      navigateBack = {
        navController.navigateUp()
      }
    )
  }
}
```

``` androidApp/HomeScreen.kt
object HomeNavGraph {
  const val home = "home"
}

fun NavGraphBuilder.homeNavGraph(
  navigateSetting: () -> Unit
) {
  composable(
    route = HomeNavGraph.home
  ) {
    HomeScreen(
      navigateSetting = navigateSetting
    )
  }
}

@Composable
private fun HomeScreen(
  navigateSetting: () -> Unit
) {
  Scaffold(
    topBar = {
      TopAppBar(
        title = {
          Text("Home")
        },
        actions = {
          IconButton(onClick = navigateSetting) {
            Icon(
              imageVector = Icons.Default.Settings,
              contentDescription = "Settings"
            )
          }
        }
      )
    }
  ) {
    Box(
      modifier = Modifier
        .fillMaxSize()
        .padding(it)
    ) {
      HomeContent(
        state = HomeState.Loaded // 検証目的のため、Stateは仮の値を入れている
      )
    }
  }
}
```

``` androidApp/SettingScreen.kt
object SettingNavGraph {
  const val setting = "setting"
}

fun NavGraphBuilder.settingNavGraph(
  navigateBack: () -> Unit
) {
  composable(
    route = SettingNavGraph.setting
  ) {
    SettingScreen(
      navigateBack = navigateBack
    )
  }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingScreen(
  navigateBack: () -> Unit
) {

  Scaffold(
    topBar = {
      TopAppBar(
        title = {
          Text("Setting")
        },
        navigationIcon = {
          IconButton(onClick = navigateBack) {
            Icon(
              imageVector = Icons.AutoMirrored.Default.ArrowBack,
              contentDescription = "Back"
            )
          }
        }
      )
    }
  ) { innerPadding ->
    SettingContent(
      modifier = Modifier.padding(innerPadding)
    )
  }
}
```

### iOS

`Compose Multiplatform` で作成した Content を　`UIViewController` にして `iOS` 側で扱えるようにし、[NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack) を用いて画面遷移を実装してました。

※ SwiftUI　で　`UIViewController` を扱うためには、`UIViewControllerRepresentable` を用いる必要がありますが、実装コードは省きます。

``` shared/iosMain/HomeContent.ios.kt
fun HomeViewController(
  state: HomeState
) = ComposeUIViewController {
  BaseTheme {
    HomeContent(
      state = state
    )
  }
}
```

``` shared/iosMain/SettingContent.ios.kt
fun SettingViewController(
  state: HomeState
) = ComposeUIViewController {
  BaseTheme {
    SettingContent()
  }
}
```

``` iosApp/iOSApp.swift
@main
struct iOSApp: App {
	var body: some Scene {
		WindowGroup {
            ContentView()
		}
	}
}
```

``` iosApp/ContentView.swift
struct ContentView: View {
    @State var path: [String] = []
    
	var body: some View {
        NavigationStack(path: $path) {
            HomeView(path: $path)
            .navigationDestination(
                for: String.self,
                destination: { appended in
                    switch appended {
                    case "home":
                        HomeView(path: $path)
                    case "setting":
                        SettingView()
                    default:
                        Text("default")
                    }
                }
            )
        }
    }
}
```

## 導入されたNavigationを使用してみる

今回導入されました、Navigationは `compose-navigation` をベースにKMPで使用できるようになっているため、Android側で行っていた実装をKMP側で行うだけで実装できます！

### ライブラリの追加

``` build.gradle.kts
kotlin {
    // ...
    sourceSets {
        // ...
        commonMain.dependencies {
            // ...
            implementation("org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha02") // MavenCentralでの公開情報を参考に適切なバージョンを入れる
        }
        // ...
    }
}
```

### KMP

KMP側にNavigationの処理も持たせ、Navigationまで含めたComposable関数を作成します。

``` shared/commonMain/App.kt
@Composable
fun App() {
  BaseTheme {
    AppNavHost()
  }
}
```

iOSで呼び出せるように、　`App` を `UIViewController` へ変換しておきます。

``` shared/iosMain/App.kt
fun MainViewController() = ComposeUIViewController { App() }
```

コードは省きますが、以下も行います。

- androidApp側で定義していた、AppNavHost.ktをKMP側に持ってくる(本記事では `kmp/app` に移動させている)
- androidApp側で定義していた、〇〇Screen.ktの実装をKMP側に持ってくる(本記事では、 `kmp/ui/feature` に移動させている)

あとは、各プラットフォームで `App` を呼び出すだけです！

### Android

``` androidApp/MainActivity.kt
class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    setContent {
      App()
    }
  }
}
```

### iOS

``` iosApp/ContentView.swift
struct ComposeAppView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        MainViewControllerKt.MainViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct ContentView: View {
	var body: some View {
        ComposeAppView().ignoresSafeArea(.all)
    }
}
```

## まとめ

KMPにNavigationの実装を持たせることで、各プラットフォームは1つのComposable関数を呼び出すだけになります！
そのため、ほぼ全て `Kotlin` だけでAndroid, iOS, Web, Desktopが実装できてしまいます。
これからもKMPは進化していくと思いますので、これからもウォッチしていきたいです！

## 注意事項

KMPのNavigation機能は、現状alpha版です。
まだ変更される可能性があるため、本番環境での利用は注意が必要です。

## 参考

- [Kotlin Multiplatform Development - Navigation and routing](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-navigation-routing.html)
- [Github - JetBrains/compose-multiplatform](https://github.com/JetBrains/compose-multiplatform)
- [sonatype - navigation-common](https://central.sonatype.com/artifact/org.jetbrains.androidx.navigation/navigation-common/overview)