Guía: Migración a la navegación de seguridad de tipos en Compose y Navigation 2

En esta guía, se describe el proceso para reemplazar las rutas basadas en cadenas por tipos serializables de Kotlin para lograr seguridad en el tiempo de compilación y eliminar las fallas en el tiempo de ejecución causadas por errores tipográficos o tipos de argumentos incorrectos.

Requisitos previos

Antes de comenzar la migración, verifica que tu proyecto cumpla con los siguientes requisitos:

  1. Versión de Navigation: Actualiza a Jetpack Navigation 2.8.0 o una versión posterior.
  2. Complemento de serialización de Kotlin:
  3. Agrega el complemento 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" }
  • Agrega las dependencias a tus archivos build.gradle.kts de nivel superior y de nivel del módulo.build.gradle.kts

Paso 1: Define tus destinos

Reemplaza las cadenas de rutas constantes por objetos y clases @Serializable.

  • Para pantallas sin argumentos: Usa un data object.
  • Para pantallas con argumentos: Usa un data class

Antes (basado en cadenas):

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

Después (con seguridad de tipos):

import kotlinx.serialization.Serializable

@Serializable
object Home

@Serializable
data class Profile(val userId: String)

Paso 2: Actualiza la configuración de NavHost

Actualiza tu NavHost para usar los nuevos tipos genéricos en las funciones composable y dialog.

Antes:

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

Después:

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

Paso 3: Implementa llamadas de navegación con seguridad de tipos

Reemplaza las llamadas de navegación interpoladas por cadenas con instancias de clase.

Antes:

navController.navigate("profile/user123")

Después:

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

Paso 4: Cómo acceder a los argumentos en ViewModels

Si usas un ViewModel, ahora puedes extraer el objeto de ruta directamente del SavedStateHandle.

Implementación:

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

Paso 5: (Avanzado) Control de tipos personalizados

Si necesitas pasar clases de datos complejas (no solo primitivas), debes definir un NavType personalizado.

  1. Crea el tipo personalizado: ```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)
) { ... }

Prácticas recomendadas y sugerencias

  • Jerarquías selladas: Para apps grandes, agrupa tus rutas con una interfaz o clase sellada para mantener organizada la estructura de navegación.
  • Instancias de objetos: Para las rutas sin parámetros, siempre usa object en lugar de class para evitar asignaciones innecesarias.
  • Tipos anulables: La nueva API admite tipos anulables (por ejemplo, data class Search(val query: String?)) y proporciona valores predeterminados automáticamente.
  • Testing: Usa navController.currentBackStackEntry?.hasRoute<T>() para verificar el destino actual de forma segura durante las pruebas de IU.