ガイド: Compose と Navigation 2 で型安全なナビゲーションに移行する

このガイドでは、コンパイル時の安全性を実現し、タイプミスや引数の型の誤りによる実行時のクラッシュをなくすために、文字列ベースのルートをシリアル化可能な Kotlin 型に置き換えるプロセスについて説明します。

前提条件

移行を開始する前に、プロジェクトが次の要件を満たしていることを確認します。

  1. Navigation のバージョン: Jetpack Navigation 2.8.0 以降に更新
  2. Kotlin シリアル化プラグイン:
  3. プラグインを libs.versions.toml に追加します。
[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }

[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
  • 最上位の build.gradle.kts とモジュール レベルの build.gradle.kts に依存関係を追加します。

ステップ 1: 宛先を定義する

定数ルート文字列を @Serializable オブジェクトとクラスに置き換えます。

  • 引数のない画面の場合: data object を使用します。
  • 引数のある画面の場合: data class を使用します。

変更前(文字列ベース):

const val ROUTE_HOME = "home"
const val ROUTE_PROFILE = "profile/{userId}"

変更後(型安全):

import kotlinx.serialization.Serializable

@Serializable
object Home

@Serializable
data class Profile(val userId: String)

ステップ 2: NavHost 構成を更新する

composable 関数と dialog 関数で新しい汎用型を使用するように NavHost を更新します。

変更前:

NavHost(navController, startDestination = "home") {
    composable("home") { HomeScreen(...) }
    composable("profile/{userId}") { backStackEntry ->
        val userId = backStackEntry.arguments?.getString("userId")
        ProfileScreen(userId)
    }
}

変更後:

NavHost(navController, startDestination = Home) {
    composable<Home> {
        HomeScreen(...)
    }
    composable<Profile> { backStackEntry ->
        // The library automatically handles argument extraction
        val profile: Profile = backStackEntry.toRoute()
        ProfileScreen(profile.userId)
    }
}

ステップ 3: 型安全なナビゲーション呼び出しを実装する

文字列補間のナビゲーション呼び出しをクラス インスタンスに置き換えます。

変更前:

navController.navigate("profile/user123")

変更後:

navController.navigate(Profile(userId = "user123"))

ステップ 4: ViewModel で引数にアクセスする

ViewModel を使用している場合は、SavedStateHandle からルート オブジェクトを直接抽出できるようになりました。

実装:

class ProfileViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    // Automatically parses arguments into the Profile class
    private val profile = savedStateHandle.toRoute<Profile>()
    val userId = profile.userId
}

ステップ 5: (高度)カスタム型の処理

プリミティブだけでなく複雑なデータクラスを渡す必要がある場合は、カスタムの NavType を定義する必要があります。

  1. カスタム型を作成する: ```kotlin val SearchFilterType = object : NavType(isNullableAllowed = false) { override fun get(bundle: Bundle, key: String): SearchFilter? = Json.decodeFromString(bundle.getString(key) ?: return null)
override fun parseValue(value: String): SearchFilter =
    Json.decodeFromString(Uri.decode(value))

override fun put(bundle: Bundle, key: String, value: SearchFilter) {
    bundle.putString(key, Json.encodeToString(value))
}

}



2. **Register it in the Graph**:
```kotlin
composable<Search>(
    typeMap = mapOf(typeOf<SearchFilter>() to SearchFilterType)
) { ... }

おすすめの方法とヒント

  • Sealed Hierarchies: 大規模なアプリでは、sealed インターフェースまたはクラスを使用してルートをグループ化し、ナビゲーション構造を整理します。
  • オブジェクト インスタンス: パラメータのないルートでは、不要な割り当てを避けるため、常に class ではなく object を使用します。
  • Nullable 型: 新しい API は nullable 型(data class Search(val query: String?) など)をサポートし、デフォルト値を自動的に提供します。
  • テスト: UI テスト中に navController.currentBackStackEntry?.hasRoute<T>() を使用して、現在の宛先をタイプセーフな方法で確認します。