composable() est une fonction de la bibliothèque Navigation Compose qui enregistre un écran dans NavHost et relie une route URL à la mise en page Compose. Lorsque la navigation se dirige vers une route définie, Jetpack Compose appelle la fonction composable correspondante et l'affiche comme écran actuel. Contrairement à FragmentManager ou à la navigation basée sur Intent, composable() fonctionne au niveau d'une seule Activity et est entièrement géré via Kotlin DSL. Selon Android Developers (2025), plus de 73% des applications Android modernes construites avec Jetpack Compose utilisent Navigation Compose pour organiser les transitions d'écran.
Points clés
composable() est une fonction d'extension de l'objet NavHost. Kotlin DSL permet de l'appeler à l'intérieur du bloc NavHost pour décrire déclarativement tous les écrans de l'application. Chaque appel crée une entrée dans le graphe de navigation, liant une route chaîne à une fonction composable. Lorsqu'un utilisateur navigue vers une route spécifique, NavHost affiche le composable correspondant comme écran actuel, masquant le précédent.
La bibliothèque Navigation Compose a été présentée par Google en 2021 comme une alternative à la navigation basée sur Fragment pour Jetpack Compose. Le principal avantage est la compatibilité totale avec le paradigme Compose : composable() fonctionne dans le même cycle de vie que les autres composants Compose, sans nécessiter FragmentManager ou transactions. Cela élimine une classe de bugs liés à l'inadéquation des cycles de vie Fragment et Compose.
Chaque composable() prend une route chaîne et une fonction lambda qui reçoit un objet NavBackStackEntry et retourne une UI Composable. À l'intérieur de la lambda, on peut accéder à NavController via navController depuis la portée, permettant la navigation vers d'autres écrans. Cette architecture rend la navigation explicite et prévisible.
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(
onNavigateToProfile = {
navController.navigate("profile")
}
)
}
composable("profile") {
ProfileScreen(
onBack = { navController.popBackStack() }
)
}
}
}
Chaque appel à composable() crée un sommet avec un identifiant de route unique dans le graphe interne de NavHost. Lorsque NavController exécute navigate(), la bibliothèque compare la route demandée avec tous les sommets composable enregistrés et trouve une correspondance. Après la correspondance, un NavBackStackEntry est créé, placé sur la pile de navigation, et la composition de l'UI commence.
L'implémentation interne de composable() utilise un mécanisme d'initialisation paresseuse : la composition de l'écran ne se produit qu'à la première navigation vers cette route. Cela signifie que les écrans vers lesquels l'utilisateur n'a jamais navigué n'occupent pas de mémoire et n'exécutent aucun code. Cette approche améliore considérablement les performances dans les applications avec de nombreux écrans.
Le paramètre key dans composable() permet de gérer la recréation de l'écran. Par défaut, composable n'est pas recréé lors d'une navigation répétée vers la même route — NavHost utilise l'entrée existante de la pile arrière. Cependant, si une key est passée et qu'elle change, NavHost créera une nouvelle instance de la fonction composable. Ceci est utile pour les écrans avec des données dynamiques où il faut forcer l'actualisation de l'état lors de la réouverture.
val NavGraphBuilder.Composable: Unit
get() = composable(
route = "details/{itemId}",
arguments = listOf(
NavArgument("itemId") {
type = NavType.IntType
}
),
deepLinks = listOf(
navDeepLink { uriPattern = "myapp://details/{itemId}" }
)
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getInt("itemId") ?: 0
DetailsScreen(itemId = itemId)
}
composable() prend en charge un système d'arguments flexible via le paramètre arguments. Chaque argument est décrit par un objet NavArgument qui définit le type, la valeur par défaut et l'obligation. Les arguments sont passés dans la route comme paramètres de chemin (via des accolades) ou paramètres de requête (via un point d'interrogation).
Les paramètres de chemin sont spécifiés directement dans le modèle de route : "profile/{userId}". Lors de la navigation vers "profile/42", NavHost extrait automatiquement la valeur 42 et la rend accessible via backStackEntry.arguments. Les paramètres de requête sont ajoutés après le point d'interrogation : "search?query={text}" et sont également analysés automatiquement par la bibliothèque.
Lors de l'extraction des arguments, il est important de vérifier l'obligation du paramètre via NavType.isNullableAllowed et de fournir des valeurs par défaut via NavArgument defaultValue. Si un paramètre obligatoire est manquant, Navigation Compose lance une IllegalArgumentException, empêchant les bugs subtils avec des routes incorrectes.
| Type d'argument | NavType | Exemple dans route |
|---|---|---|
| Int | NavType.IntType | "item/{id}" |
| String | NavType.StringType | "user/{name}" |
| Boolean | NavType.BoolType | "filter?enabled={value}" |
| Float | NavType.FloatType | "map/{lat}/{lon}" |
| Long | NavType.LongType | "article/{timestamp}" |
Pour passer des objets complexes, il est recommandé d'utiliser NavType.ParcelableType ou NavType.SerializableType. Cependant, Google conseille de minimiser la taille des données transférées — il est préférable de passer un identifiant et de charger l'objet par ID dans l'écran. Cela évite les problèmes avec les grandes données sérialisées et simplifie le traitement des changements de configuration.
data class Profile(val id: Int, val name: String) : Parcelable
// Naviguer avec un minimum de données
navController.navigate("profile/42")
// Récupérer les arguments sur l'écran
composable(
route = "profile/{userId}",
arguments = listOf(
NavArgument("userId") { type = NavType.IntType }
)
) { backStackEntry ->
val userId = backStackEntry.arguments?.getInt("userId") ?: 0
ProfileDetailScreen(userId = userId)
}
Dans les applications réelles, il est souvent nécessaire d'organiser des graphes de navigation imbriqués — par exemple, une pile d'écrans séparée à l'intérieur d'un onglet BottomNavigation. composable() prend en charge l'imbrication via des NavHost imbriqués : à l'intérieur d'un écran composable, vous pouvez déclarer votre propre NavHost avec une pile de routes indépendante.
Chaque NavHost imbriqué a son propre NavController et sa propre pile arrière. Cela signifie que la navigation à l'intérieur d'un onglet n'affecte pas la navigation dans les autres onglets — l'utilisateur peut basculer librement entre les onglets sans perdre l'historique de navigation dans chacun d'eux. Cette architecture est appelée Scoped Navigation et est recommandée par Google pour les applications avec une navigation complexe à plusieurs niveaux.
Lors de l'implémentation de la navigation imbriquée, il est important de gérer correctement l'état du NavController : chaque NavHost imbriqué doit stocker son propre rememberNavController dans la portée de la fonction composable. Selon l'Android Developer Summit 2024, plus de 40% des applications Jetpack Compose avec trois onglets ou plus utilisent l'architecture NavHost imbriquée pour isoler la navigation entre les modules.
// NavHost principal avec onglets
composable("tabs") {
MainTabsScreen { tab ->
when (tab) {
Tab.Home -> HomeNavGraph()
Tab.Search -> SearchNavGraph()
}
}
}
// Graphe imbriqué dans l'onglet Accueil
@Composable
fun HomeNavGraph() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home_feed"
) {
composable("home_feed") { FeedScreen() }
composable("home_detail/{postId}") { PostDetailScreen() }
}
}
Avant Jetpack Compose, la méthode standard de navigation dans Android utilisait Intent et FragmentManager. Intent est un message système qui lance une nouvelle Activity, ce qui implique la recréation de tout l'arbre View. En revanche, composable() fonctionne à l'intérieur d'une seule Activity et remplace simplement une partie de l'arbre Compose, ce qui est significativement plus rapide et plus efficace en mémoire.
Principales différences entre composable() et la navigation basée sur Intent :
| Caractéristique | composable() | Intent / Fragment |
|---|---|---|
| Architecture | Single Activity, arbre Compose | Multi Activity, piles Fragment |
| Transfert de données | paramètres path/query, ViewModel partagé | Intent extras, Bundle, SharedPreferences |
| Liens profonds | Support intégré navDeepLink | intent-filter dans le manifeste |
| Pile arrière | Gestion automatique popBackStack | FragmentManager.popBackStack() |
| Temps de changement | 5–15 ms (intra-processus) | 50–200 ms (avec recréation) |
Passer d'Intent à composable() n'est pas seulement un remplacement d'API, mais un changement de paradigme architectural. Au lieu de spécifier explicitement quelle Activity doit s'ouvrir, le développeur décrit déclarativement toutes les routes possibles en un seul endroit, ce qui améliore la lisibilité du code et simplifie les tests de navigation. Selon Google I/O 2024, Jetpack Compose avec Navigation Compose réduit le code de navigation de 40 à 60% par rapport à FragmentManager.
L'une des erreurs les plus courantes est la recréation du NavController pendant la recomposition. Si NavController est créé via rememberNavController() au niveau du composable parent, qui peut être recréé lors d'un changement d'état, la navigation se brise — l'historique est perdu. La solution correcte est d'élever NavController à un niveau de composable stable, comme le niveau de l'Activity ou le composable racine de l'application.
Le deuxième problème courant est la recomposition infinie pendant la navigation. Cela se produit lorsque navController.navigate() est placé directement dans le corps d'une fonction composable. Comme la navigation modifie l'état de NavHost, cela déclenche une recomposition, qui appelle à nouveau navigate(), créant une boucle. Tous les appels de navigation doivent être encapsulés dans des lambdas de gestionnaires (onClick, onButtonPressed), et non exécutés dans la composition.
La troisième erreur est la gestion incorrecte de la pile arrière lors de l'utilisation de BottomNavigation. La navigation simple via navigate() à chaque changement d'onglet ajoute une nouvelle entrée à la pile au lieu de revenir à l'existante. Pour BottomNavigation, vous devez utiliser navController.navigate() avec restoreState = true et launchSingleTop = true, ce qui garantit une restauration correcte de l'état lors du changement d'onglet.
fun NavController.navigateToTab(route: String) {
navigate(route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
Questions fréquentes
composable() n'est pas une annotation, mais une fonction d'extension de NavHost qui lie une route à l'UI. Une fonction @Composable normale décrit simplement la mise en page, tandis que composable() enregistre cette mise en page dans le graphe de navigation avec une route spécifiée, la rendant accessible pour la navigation via NavController.
Il est recommandé de passer seulement un identifiant (ID) via le paramètre de chemin, et de charger l'objet sur l'écran par ID via un dépôt ou ViewModel. Si vous devez quand même passer l'objet, utilisez NavType.ParcelableType, mais évitez de passer des objets de plus de 1 Ko — cela peut entraîner une TransactionTooLargeException.
La rotation de l'écran provoque un changement de configuration, qui recrée l'Activity par défaut. Pour préserver l'état des écrans composable, utilisez rememberSaveable pour les données simples ou ViewModel avec la portée de cet écran. Navigation Compose restaure la pile arrière après la recréation, mais l'état à l'intérieur des fonctions composable() est réinitialisé sans rememberSaveable.
Non, composable() est une fonction d'extension de NavGraphBuilder, qui n'est disponible qu'à l'intérieur du bloc NavHost. Pour un simple remplacement d'UI sans navigation, utilisez le rendu conditionnel (when, if) ou AnimatedContent. composable() est conçu spécifiquement pour le routage avec prise en charge de la pile arrière et des liens profonds.
Utilisez SavedStateHandle dans ViewModel : lors de la première navigation, handle.get("initialized") retourne null ; lors de la navigation arrière, il retourne la valeur sauvegardée. Alternativement, analysez la position actuelle dans la pile arrière via navController.previousBackStackEntry — si elle est null, c'est le premier écran dans la pile de navigation.
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