Anleitung: Zur typsicheren Navigation in Compose und Navigation 2 migrieren

In diesem Leitfaden wird beschrieben, wie Sie stringbasierte Routen durch serialisierbare Kotlin-Typen ersetzen, um die Kompilierzeitsicherheit zu erhöhen und Laufzeitabstürze zu vermeiden, die durch Tippfehler oder falsche Argumenttypen verursacht werden.

Voraussetzungen

Prüfen Sie vor Beginn der Migration, ob Ihr Projekt die folgenden Anforderungen erfüllt:

  1. Navigationsversion: Update auf Jetpack Navigation 2.8.0 oder höher
  2. Kotlin-Serialisierungs-Plug-in:
  3. Fügen Sie das Plug-in zu libs.versions.toml hinzu:
[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" }
  • Fügen Sie die Abhängigkeiten der Datei build.gradle.kts auf oberster Ebene und der Datei build.gradle.kts auf Modulebene hinzu.

Schritt 1: Ziele definieren

Ersetzen Sie Ihre konstanten Routenstrings durch @Serializable-Objekte und -Klassen.

  • Für Bildschirme ohne Argumente: Verwenden Sie ein data object.
  • Für Bildschirme mit Argumenten: Verwenden Sie eine data class.

Vorher (String-basiert):

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

Nachher (typsicher):

import kotlinx.serialization.Serializable

@Serializable
object Home

@Serializable
data class Profile(val userId: String)

Schritt 2: NavHost-Konfiguration aktualisieren

Aktualisieren Sie NavHost, um die neuen generischen Typen in den Funktionen composable und dialog zu verwenden.

Vorher:

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

Nachher:

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

Schritt 3: Typsichere Navigationsaufrufe implementieren

Ersetzen Sie String-interpolierte Navigationsaufrufe durch Klasseninstanzen.

Vorher:

navController.navigate("profile/user123")

Nachher:

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

Schritt 4: Auf Argumente in ViewModels zugreifen

Wenn Sie ein ViewModel verwenden, können Sie das Routenobjekt jetzt direkt aus dem SavedStateHandle extrahieren.

Implementierung:

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

Schritt 5 (erweitert): Benutzerdefinierte Typen verarbeiten

Wenn Sie komplexe Datenklassen (nicht nur Primitiven) übergeben müssen, müssen Sie eine benutzerdefinierte NavType definieren.

  1. Benutzerdefinierten Typ erstellen: ```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)
) { ... }

Best Practices und Tipps

  • Versiegelte Hierarchien: Bei großen Apps sollten Sie Ihre Routen mit einer versiegelten Schnittstelle oder Klasse gruppieren, um die Navigationsstruktur zu organisieren.
  • Objektinstanzen: Verwenden Sie für Routen ohne Parameter immer object anstelle von class, um unnötige Zuweisungen zu vermeiden.
  • Nullable-Typen: Die neue API unterstützt Nullable-Typen (z. B. data class Search(val query: String?)) und stellt automatisch Standardwerte bereit.
  • Tests: Mit navController.currentBackStackEntry?.hasRoute<T>() können Sie das aktuelle Ziel während der UI-Tests typsicher prüfen.