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 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.
@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.
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() 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éthode | Description | Exemple |
|---|---|---|
| popBackStack() | Retour d'un écran en arrière | navController.popBackStack() |
| popBackStack(route, false) | Nettoyer jusqu'à route (route reste) | popBackStack(«home», false) |
| popBackStack(route, true) | Nettoyer jusqu'à et y compris route | popBackStack(«home», true) |
| navigate(route) { popUpTo(route) { inclusive = true } } | Naviguer avec nettoyage complet | navigate(«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 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.
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.
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.
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
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).
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.
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.
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.
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é
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.
Lisez aussi