NavController — essence, méthodes et gestion de la navigation dans Jetpack Compose

Auteur : IT Sectr Publié le : 2026-06-29 Temps de lecture : 7 min

NavController est le composant central de la bibliothèque Navigation Compose qui gère la pile de navigation et l'état du back stack dans les applications Android. Via NavController, les transitions entre écrans, le retour aux pages précédentes et le transfert de données entre routes sont effectués. Selon Android Developers (2025), NavController est un élément obligatoire de toute application Compose avec plus d'un écran. Le contrôleur est créé via rememberNavController(), passé à NavHost et disponible pour appeler navigate() depuis n'importe quel point de la composition. La prise en charge intégrée de SavedStateHandle sauvegarde automatiquement l'état du ViewModel lors de la reconfiguration.

Points Clés

  • NavController — le contrôleur de navigation central de Compose, gérant le back stack et les transitions entre écrans
  • navigate() — la méthode principale pour naviguer vers une route avec prise en charge de NavOptions pour la gestion de la pile
  • popBackStack() — retour à l'écran précédent avec nettoyage optionnel jusqu'à une route spécifiée
  • SavedStateHandle — intégration avec ViewModel pour préserver l'état de l'écran pendant la navigation
  • currentBackStackEntryAsState() — observation de la route actuelle pour la synchronisation de l'UI

Qu'est-ce que NavController dans Jetpack Compose ?

NavController est une classe de la bibliothèque Navigation Compose qui implémente le contrôleur de navigation pour les applications Compose. NavController gère la pile NavBackStackEntry, où chaque entrée contient la route, les arguments et l'état de l'écran. Le contrôleur prend en charge les opérations de navigation de base : transition, retour, remplacement et nettoyage.

Contrairement au système View, où la navigation se faisait via FragmentManager ou Intent, NavController fonctionne exclusivement dans le contexte Compose. Le back stack est stocké sous forme de graphe NavDestination plutôt que de pile de Fragment. Cela élimine le surcoût de création et de destruction de Fragment et simplifie les tests — NavController peut être mocké via TestNavHostController.

NavController est étroitement lié à NavHost — un conteneur qui affiche l'écran actuel du graphe. Sans NavHost, NavController ne peut pas afficher de fonctions composables mais conserve la capacité de gérer la pile. Dans une architecture typique, NavController est créé au niveau de l'Activity ou du composable principal et transmis vers le bas de l'arbre de composition via des paramètres.

Selon Google, NavController a connu plusieurs versions majeures. La version 2.8.0 a ajouté la navigation Type-Safe, la version 2.9.0 a ajouté la prise en charge du predictive back gesture (Android 14+). Le contrôleur est compatible avec Material3 Scaffold et BottomNavigation. Pour les projets multimodules, NavController est transmis via DI (Hilt/Koin) ou des paramètres de constructeur.

NavController est créé via la fonction composable rememberNavController(). La fonction retourne une instance de NavHostController (une sous-classe de NavController) liée au cycle de vie du composable actuel. En quittant la composition, le contrôleur est nettoyé. Pour préserver le contrôleur lors de la reconfiguration, utilisez rememberSaveable ou ViewModel.

kotlin
@Composable
fun MyApp() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = "main"
    ) {
        composable("main") { MainScreen(navController) }
        composable("details") { DetailsScreen(navController) }
    }
}

La configuration de NavController comprend : NavHostController (principal), TestNavHostController (test) et ScopedNavController (enfant pour les graphes imbriqués). Pour BottomNavigation, NavController doit être unique pour toute l'application — créer un nouveau contrôleur dans chaque onglet entraînera une perte de pile. Pour transmettre le contrôleur aux écrans imbriqués, utilisez un paramètre de fonction plutôt que CompositionLocalProvider pour maintenir la lisibilité.

Pour tester la navigation, utilisez TestNavHostController avec compose-test-rule. Le contrôleur permet de définir la route initiale et de vérifier que navigate() a déclenché la transition attendue. Tester NavController ne nécessite pas d'émulateur — il fonctionne avec les matchers Semantics de Compose Test.

La méthode navigate(route: String) est le mécanisme principal de navigation dans NavController. Elle accepte une chaîne de route, des NavOptions optionnels et Navigator.Extras. NavOptions contrôlent le comportement de la transition : launchSingleTop (ne pas dupliquer la route dans la pile), popUpTo (nettoyer la pile jusqu'à une route), restoreState (restaurer l'état précédent).

Les NavOptions sont définis via la syntaxe builder : NavOptionsBuilder. Paramètres principaux : popUpTo (route + inclusive/saveState), launchSingleTop (Boolean, true — ne pas créer de doublons), restoreState (restaurer l'état au retour). Sans popUpTo, chaque navigate() ajoute une entrée à la pile, entraînant une accumulation du back stack et un comportement incorrect du bouton Back.

kotlin
navController.navigate("profile/42") {
    popUpTo("main") { saveState = true }
    launchSingleTop = true
    restoreState = true
}

Navigator.Extras permet de passer des données supplémentaires ne faisant pas partie de la route : éléments partagés pour l'animation, indicateurs Intent, bundle Pac-Man. Les Extras sont rarement utilisés — principalement pour l'intégration avec Accompanist Animation ou des Navigators personnalisés. Pour la plupart des scénarios, une chaîne de route et NavOptions suffisent.

popBackStack : gestion du retour et du nettoyage de la pile

popBackStack() est la méthode pour revenir à l'écran précédent. Sans arguments, elle supprime l'entrée du sommet de la pile et retourne true si la suppression a réussi. Si la pile est vide, la méthode retourne false et l'Activity se ferme (similaire à super.onBackPressed()).

La version surchargée popBackStack(route: String, inclusive: Boolean) supprime toutes les entrées jusqu'à la route spécifiée. Si inclusive = true, la route spécifiée elle-même est également supprimée. La méthode retourne Boolean — true si des entrées ont été trouvées et supprimées. La version avec inclusive est utile pour les scénarios de « sortie vers l'écran racine » après autorisation ou finalisation de commande.

MéthodeDescriptionExemple
popBackStack()Retour d'un écran en arrièrenavController.popBackStack()
popBackStack(route, false)Nettoyer jusqu'à route (route reste)popBackStack(«home», false)
popBackStack(route, true)Nettoyer jusqu'à et y compris routepopBackStack(«home», true)
navigate(route) { popUpTo(route) { inclusive = true } }Naviguer avec nettoyage completnavigate(«login») { popUpTo(0) { inclusive = true } }

Pour gérer le bouton Back système (hardware back button), utilisez BackHandler de Compose. BackHandler accepte enabled et onBack — un callback invoqué lors de l'appui. Pour Android 14+, PredictiveBackGesture est utilisé, intégré via NavController depuis la version 2.9.0. Predictive back ajoute une animation de prévisualisation du retour.

SavedStateHandle : préservation de l'état de l'écran

SavedStateHandle est un mécanisme de préservation de l'état du ViewModel pendant la navigation et la reconfiguration. NavController fournit automatiquement SavedStateHandle pour chaque NavBackStackEntry. Via SavedStateHandle, ViewModel stocke l'état de l'écran et le restaure au retour (restoreState = true).

Dans Navigation Compose, SavedStateHandle est utilisé conjointement avec ViewModel : ViewModel est initialisé via SavedStateHandle, qui est passé depuis backStackEntry. En naviguant vers un autre écran et en revenant (avec restoreState), ViewModel reçoit l'état sauvegardé plutôt que d'être créé à nouveau. C'est crucial pour les écrans avec saisie de données, filtres ou défilement.

kotlin
class ProfileViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    val userId: String = savedStateHandle.get<String>("userId") ?: ""
    var searchQuery by savedStateHandle.getStateFlow("search", "")
        .collectAsState()
}

SavedStateHandle prend en charge les types primitifs, String, Bundle et Parcelable. Pour les objets complexes, sauvegardez uniquement les IDs et chargez les données complètes depuis le dépôt. La limite de SavedStateHandle est d'environ 1 Mo, son dépassement provoque TransactionTooLargeException. Pour les grands volumes, utilisez Room ou DataStore plutôt que de sauvegarder dans le handle.

Important : SavedStateHandle préserve l'état uniquement lorsque restoreState = true est utilisé dans NavOptions. Si restoreState n'est pas spécifié, ViewModel est créé à nouveau avec des valeurs par défaut au retour. Pour la commutation de BottomNavigation avec restoreState, NavController préserve l'état de chaque onglet et le restaure lors de la resélection.

Observation de la route actuelle via currentBackStackEntryAsState

currentBackStackEntryAsState() est une fonction qui retourne State<NavBackStackEntry?>, qui se met à jour à chaque changement de la route actuelle. C'est le mécanisme principal de synchronisation de l'UI avec la navigation : BottomNavigation met en évidence l'élément actif, Toolbar met à jour le titre, Drawer se ferme lors de la transition.

La fonction fonctionne via snapshotFlow et collectAsState : lorsque le back stack change, Compose recompose les éléments abonnés. Important : currentBackStackEntryAsState() se met à jour seulement après la fin de l'animation de transition. Pour des mises à jour immédiates, utilisez currentDestination, qui change de manière synchrone avec navigate() mais ne prend pas en charge l'état.

kotlin
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

Text(
    text = when (currentRoute) {
        "home" -> "Home"
        "profile" -> "Profile"
        else -> ""
    }
)

Pour accéder aux arguments de la route actuelle, utilisez navBackStackEntry?.arguments. C'est pratique dans BottomNavigation : selectedItem est calculé en fonction de currentRoute. Pour le débogage de la navigation, utilisez NavController.addOnDestinationChangedListener() qui enregistre chaque transition. En production, évitez les abonnements dans un grand nombre de composables — créez une source unique dans ViewModel et passez State à l'UI.

Foire Aux Questions

Peut-on créer plusieurs NavControllers dans une seule Activity ?

Techniquement oui, mais ce n'est pas recommandé. Un seul NavController garantit un back stack cohérent et simplifie le débogage. Plusieurs contrôleurs sont justifiés seulement pour les graphes imbriqués avec navigation séparée (par exemple, modal bottom sheet avec sa propre pile).

Comment passer NavController via ViewModel ?

Passez NavController à ViewModel via le constructeur ou DI. Cependant, il est préférable de passer uniquement des fonctions callback (onNavigate, onBack) plutôt que NavController lui-même — cela simplifie les tests. Pour les événements, utilisez Channel<NavEvent> dans ViewModel et collectez dans l'UI.

Pourquoi navigate ne fonctionne-t-il pas après une opération asynchrone ?

Le problème vient du cycle de vie : si NavController n'est pas encore initialisé (NavHost pas construit), navigate() est ignoré. Utilisez LaunchedEffect pour appeler la navigation après le chargement des données, pas à l'intérieur d'une coroutine avec un cycle de vie arbitraire.

Comment nettoyer tout le back stack et naviguer vers un nouvel écran ?

Appelez navController.navigate(«cible») { popUpTo(0) { inclusive = true } }. Le paramètre popUpTo(0) nettoie la pile complètement, inclusive = true supprime également l'entrée de départ. Le drapeau launchSingleTop = true empêche les routes en double.

Quelle est la différence entre NavHostController et NavController ?

NavHostController est une sous-classe de NavController avec des méthodes supplémentaires pour NavHost (par exemple, setOnBackStackChangedListener). NavController est la classe de base qui peut être utilisée en dehors de NavHost pour une gestion programmatique de la pile. Dans la plupart des cas, NavHostController est utilisé.

Résumé

  • NavController — le composant central de Navigation Compose, gérant la pile de routes et les transitions entre écrans
  • navigate() effectue des transitions avec les paramètres popUpTo, launchSingleTop et restoreState via NavOptions
  • popBackStack() gère le retour : étape unique ou nettoyage en masse jusqu'à une route spécifiée avec inclusive
  • SavedStateHandle s'intègre avec ViewModel pour la préservation automatique de l'état de l'écran pendant la navigation
  • currentBackStackEntryAsState() fournit une observation réactive de la route actuelle pour la synchronisation de l'UI
  • BackHandler gère le bouton Back système, et PredictiveBackGesture est pris en charge depuis NavController 2.9.0
  • Pour les tests, utilisez TestNavHostController avec compose-test-rule et les matchers Semantics

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi