Guide : Migrer vers la navigation avec sûreté du typage dans Compose et Navigation 2

Ce guide explique comment remplacer les routes basées sur des chaînes par des types Kotlin sérialisables pour assurer la sécurité au moment de la compilation et éliminer les plantages d'exécution causés par des fautes de frappe ou des types d'arguments incorrects.

Prérequis

Avant de commencer la migration, vérifiez que votre projet répond aux exigences suivantes :

  1. Version de Navigation : passez à Jetpack Navigation 2.8.0 ou version ultérieure.
  2. Plug-in de sérialisation Kotlin :
  3. Ajoutez le plug-in à 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" }
  • Ajoutez les dépendances à votre fichier build.gradle.kts de premier niveau et à votre fichier build.gradle.kts au niveau du module.

Étape 1 : Définissez vos destinations

Remplacez vos chaînes de route constantes par des objets et des classes @Serializable.

  • Pour les écrans sans arguments : utilisez un data object.
  • Pour les écrans avec des arguments : utilisez un data class.

Avant (basé sur une chaîne) :

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

Après (type sécurisé) :

import kotlinx.serialization.Serializable

@Serializable
object Home

@Serializable
data class Profile(val userId: String)

Étape 2 : Mettez à jour la configuration NavHost

Mettez à jour votre NavHost pour utiliser les nouveaux types génériques dans la fonction composable et dialog.

Avant :

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

Aprè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)
    }
}

Étape 3 : Implémenter des appels de navigation avec sûreté du typage

Remplacez les appels de navigation interpolés par des instances de classe.

Avant :

navController.navigate("profile/user123")

Après :

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

Étape 4 : Accéder aux arguments dans les ViewModel

Si vous utilisez un ViewModel, vous pouvez désormais extraire l'objet route directement à partir de SavedStateHandle.

Implémentation :

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

Étape 5 : (Avancé) Gérer les types personnalisés

Si vous devez transmettre des classes de données complexes (et pas seulement des primitives), vous devez définir un NavType personnalisé.

  1. Créez le type personnalisé : ```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)
) { ... }

Bonnes pratiques et conseils

  • Hiérarchies scellées : pour les grandes applications, regroupez vos routes à l'aide d'une interface ou d'une classe scellée afin de maintenir la structure de navigation organisée.
  • Instances d'objet : pour les routes sans paramètres, utilisez toujours object au lieu de class pour éviter les allocations inutiles.
  • Types pouvant être nuls : la nouvelle API accepte les types pouvant être nuls (par exemple, data class Search(val query: String?)) et fournit automatiquement des valeurs par défaut.
  • Test : utilisez navController.currentBackStackEntry?.hasRoute<T>() pour vérifier la destination actuelle de manière type-safe lors des tests d'UI.