Guida: Migrazione alla navigazione con controllo del tipo in Compose e Navigation 2

Questa guida descrive il processo di sostituzione delle route basate su stringhe con tipi Kotlin serializzabili per ottenere la sicurezza in fase di compilazione ed eliminare gli arresti anomali del runtime causati da errori di battitura o tipi di argomenti errati.

Prerequisiti

Prima di iniziare la migrazione, verifica che il progetto soddisfi i seguenti requisiti:

  1. Versione di Navigation: esegui l'aggiornamento a Jetpack Navigation 2.8.0 o versioni successive
  2. Plug-in di serializzazione Kotlin:
  3. Aggiungi il plug-in a 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" }
  • Aggiungi le dipendenze a build.gradle.kts di primo livello e a build.gradle.kts a livello di modulo.

Passaggio 1: definisci le destinazioni

Sostituisci le stringhe di route costanti con oggetti e classi @Serializable.

  • Per le schermate senza argomenti: utilizza un data object
  • Per le schermate con argomenti: utilizza un data class

Prima (basato su stringa):

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

Dopo (type safe):

import kotlinx.serialization.Serializable

@Serializable
object Home

@Serializable
data class Profile(val userId: String)

Passaggio 2: aggiorna la configurazione di NavHost

Aggiorna NavHost per utilizzare i nuovi tipi generici nelle funzioni composable e dialog.

Prima:

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

Dopo:

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

Passaggio 3: implementa chiamate di navigazione type-safe

Sostituisci le chiamate di navigazione con interpolazione di stringhe con istanze di classe.

Prima:

navController.navigate("profile/user123")

Dopo:

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

Passaggio 4: accesso agli argomenti nei ViewModel

Se utilizzi un ViewModel, ora puoi estrarre l'oggetto route direttamente da SavedStateHandle.

Implementazione:

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

Passaggio 5: (avanzato) gestione dei tipi personalizzati

Se devi trasmettere classi di dati complesse (non solo primitive), devi definire un NavType personalizzato.

  1. Crea il tipo personalizzato: ```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 practice e suggerimenti

  • Gerarchie sigillate: per le app di grandi dimensioni, raggruppa le route utilizzando un'interfaccia o una classe sigillata per mantenere organizzata la struttura di navigazione
  • Istanze oggetto: per le route senza parametri, utilizza sempre object anziché class per evitare allocazioni non necessarie
  • Tipi Nullable: la nuova API supporta i tipi Nullable (ad esempio, data class Search(val query: String?)) e fornisce automaticamente i valori predefiniti
  • Test: utilizza navController.currentBackStackEntry?.hasRoute<T>() per controllare la destinazione corrente in modo type-safe durante i test della UI