Przewodnik: migracja do nawigacji z bezpieczeństwem typów w Compose i Navigation 2

Ten przewodnik opisuje proces zastępowania tras opartych na ciągach znaków serializowalnymi typami Kotlin, aby zapewnić bezpieczeństwo w czasie kompilacji i wyeliminować awarie w czasie działania spowodowane literówkami lub nieprawidłowymi typami argumentów.

Wymagania wstępne

Przed rozpoczęciem migracji sprawdź, czy Twój projekt spełnia te wymagania:

  1. Wersja nawigacji: zaktualizuj nawigację Jetpack do wersji 2.8.0 lub nowszej.
  2. Wtyczka serializacji Kotlin:
  3. Dodaj wtyczkę do 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" }
  • Dodaj zależności do plików build.gradle.kts najwyższego poziomu i build.gradle.kts na poziomie modułu.

Krok 1. Określ miejsca docelowe

Zastąp stałe ciągi znaków trasy obiektami i klasami @Serializable.

  • W przypadku ekranów bez argumentów: użyj data object
  • W przypadku ekranów z argumentami: użyj data class

Przed (na podstawie ciągu znaków):

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

Po (bezpieczne typy):

import kotlinx.serialization.Serializable

@Serializable
object Home

@Serializable
data class Profile(val userId: String)

Krok 2. Zaktualizuj konfigurację NavHost

Zaktualizuj NavHost, aby używać nowych typów ogólnych w funkcji composabledialog.

Przed:

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

Po:

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

Krok 3. Wdróż wywołania nawigacji bezpieczne pod względem typów

Zastąp wywołania nawigacji z interpolacją ciągów instancjami klas.

Przed:

navController.navigate("profile/user123")

Po:

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

Krok 4. Dostęp do argumentów w klasach ViewModel

Jeśli używasz ViewModel, możesz teraz wyodrębnić obiekt trasy bezpośrednio z SavedStateHandle.

Implementacja:

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

Krok 5. (Zaawansowane) Obsługa typów niestandardowych

Jeśli musisz przekazywać złożone klasy danych (nie tylko typy proste), musisz zdefiniować niestandardowy NavType.

  1. Utwórz typ niestandardowy: ```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)
) { ... }

Sprawdzone metody i wskazówki

  • Zamknięte hierarchie: w przypadku dużych aplikacji grupuj trasy za pomocą zamkniętego interfejsu lub klasy, aby zachować porządek w strukturze nawigacji.
  • Instancje obiektów: w przypadku tras bez parametrów zawsze używaj object zamiast class, aby uniknąć niepotrzebnych przydziałów.
  • Typy dopuszczające wartość null: nowy interfejs API obsługuje typy dopuszczające wartość null (np. data class Search(val query: String?)) i automatycznie podaje wartości domyślne.
  • Testowanie: użyj navController.currentBackStackEntry?.hasRoute<T>(), aby podczas testów interfejsu sprawdzić bieżące miejsce docelowe w sposób bezpieczny pod względem typów.