Navigation multi-écrans¶
La navigation¶
Beaucoup d'applications mobiles nécessitent un système de navigation pour gérer comment l'application passe d'un écran à l'autre.
Je vous propose ici une technique à appliquer dans une application mobile Android avec JetPack Compose qui utilise l'API NavController .
Dans cette fiche :
Ajout de dépendances¶
NavController¶
NavHost¶
Affichage de la page actuelle¶
Naviguer vers une page¶
Passer des paramètres à une route¶
Route avec paramètres de types différents¶
Route avec paramètres optionnels¶
Ajout de dépendances¶
Pour ajouter une fonctionnalité de navigation dans votre application, vous devez d'abord ajouter une dépendance au projet.
Dans le fichier build.gradle.kts qui se trouve dans le dossier app , ajoutez ceci :
dependencies {
...
// pour la navigation
implementation("androidx.navigation:navigation-compose:2.10.2")
}
Une fois la dépendance ajoutée, il faut resynchroniser le projet pour qu'il tienne compte de l'ajout.
NavController¶
Votre application doit avoir accès à une instance de NavController.
L'instanciation doit avoir lieu dans un composable. Il faut choisir l'endroit le plus près de où on en aura besoin.
Dans cet exemple, le Scaffold est le premier composable qui a besoin du NavController.
import androidx.navigation.compose.rememberNavController
...
val navController = rememberNavController()
Scaffold(
...
)
NavHost¶
La liste des composables qui peuvent être rejoints par navigation sera définie dans un composable NavHost. Cette liste est en fait une liste des routes possibles dans l'application. Ces routes sont parfois appelées itinéraires ou destinations.
La navigation est en fait une pile de routes. La route qui est affichée est celle qui se trouve au sommet de la pile. Lorsque l'on navigue vers une nouvelle route, elle est ajoutée au sommet de la pile. Lorsque l'on revient à la route précédente, la route du sommet est retirée de la pile et la route précédente devient visible.
Le NavHost affiche la composante correspondant à la route qui se trouve au sommet.
Le NavHost sera placé dans une fonction composable NavigationHost que l'on codera dans son propre fichier NavigationHost.kt, placé au même niveau que MainActivity.kt .
Pour chaque route, on spécifiera le nom qui sera utilisé pour la rejoindre puis le nom de la fonction composable à appeler.
Lorsque l'application contient une barre de navigation, la route passera le navController en paramètre seulement si la fonction composable a besoin de travailler avec cette variable.
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
...
@Composable
fun NavigationHost (navController: NavHostController) {
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen (...)
}
composable("pageUn") {
PageUn (...)
}
composable("pageDeux") {
PageDeux (...)
}
}
}
Il est d'usage de placer chaque fonction composable de cette liste dans son propre fichier, sous le dossier ui. Le nom du fichier sera le même que le nom de la fonction.
Affichage de la page actuelle¶
Dans le Scaffold, c'est le NavigationHost qui indiquera quelle page doit être affichée.
Remarquez l'utilisation du Column qui permet de spécifier les espacements à utiliser pour toutes les pages.
D'autres configurations peuvent y être apportées au besoin.
val navController = rememberNavController()
Scaffold(
...
) {
// contenu du Scaffold
Column(
modifier = Modifier.padding(it)
) {
NavigationHost(navController = navController)
}
}
Naviguer vers une page¶
La méthode navController.navigate permet d'atteindre la page souhaitée et de l'ajouter à la pile des pages affichées.
Button(
onClick = {
navController.navigate("pageUn")
}
) {
Text(text = "Page un")
}
Pour revenir à la page d'avant et ainsi la sortir de la pile, on utilisera popBackStack().
Passer des paramètres à une route¶
Pour définir une route qui peut recevoir un paramètre, on utilise des accolades dans le nom de la route. Dans cet exemple, le paramètre est nommé texte:
NavHost(navController = navController, startDestination = "home") {
...
composable("rechercherItem/{texte}") { navBackStackEntry ->
// Extraire le paramètre à partir de la route
// .getString() retourne un String? (nullable)
val texte:String? = navBackStackEntry.arguments?.getString("texte")
// Passer le paramètre à la fonction composable
// requireNotNull() permet de transformer un String? en String
// Ici on sait que le paramètre ne peut pas être null car il est obligatoire dans la route
RechercherItem(navController, requireNotNull(texte))
}
}
Le composable recevra le paramètre comme argument.
@Composable
fun RechercherItem(navController: NavController, texte: String) {
...
}
Pour naviguer vers une route avec paramètre :
val texteRecherche = "exemple"
navController.navigate("rechercherItem/$texteRecherche")
Route avec paramètres de types différents¶
Par défaut, les paramètres sont des chaînes de caractères (String). Dans ce cas, les noms de paramètres sont extraits de la route automatiquement.
Si vous avez besoin d'un paramètre d'un autre type, par exemple un entier, vous devez fournir le paramètre arguments à la route. Dans cet exemple, le paramètre itemId est de type entier.:
NavHost(navController = navController, startDestination = "home") {
...
composable(
route = "editerItem/{itemId}",
arguments = listOf(
navArgument("itemId") { type = NavType.IntType }
)
) { navBackStackEntry ->
// extraire le paramètre à partir de la route
val itemId: Int = navBackStackEntry.arguments?.getInt("itemId") ?: -1
// passer le paramètre à la fonction composable
EditerItem(navController, itemId)
}
}
Pour naviguer vers cette route :
Route avec paramètres optionnels¶
Si le paramètre est optionnel :
NavHost(navController = navController, startDestination = "home") {
...
composable(
route = "detailsItem?itemId={itemId}",
arguments = listOf(
navArgument("itemId") {
type = NavType.StringType
nullable = true
defaultValue = null
}
)
) { navBackStackEntry ->
// extraire le paramètre à partir de la route
val itemId: String? = navBackStackEntry.arguments?.getString("itemId")
// passer le paramètre à la fonction composable
DetailsItem(navController, itemId)
}
}
Le paramètre de DetailsItem doit être de type String? (nullable).
@Composable
fun DetailsItem(navController: NavController, itemId: String?) {
if (itemId != null) {
Text(text = "Détails de l'élément $itemId")
} else {
Text(text = "Aucun élément sélectionné")
}
}
Cette fois, il sera possible de ne pas passer de paramètre au besoin.
navController.navigate("detailsItem?itemId=3")
...
// Page de détails vide
navController.navigate("detailsItem")
Pour plus d'information¶
-
« Navigation et pile "Retour" » - Android Developers](https://developer.android.com/guide/navigation/backstack?hl=fr)
Le ViewModel et la navigation¶
On sait que dans une application, le ViewModel ne doit exister qu'en un seul exemplaire. Il doit donc être instancié à l'endroit approprié puis passé en paramètre aux fonctions composables qui en ont besoin.
Dans le cas où une application qui travaille avec un ViewModel a besoin de navigation, une solution consiste à déclarer le ViewModel dans le NavigationHost puis à le passer en paramètre aux composables dans les routes où c'est nécessaire.
@Composable
fun NavigationHost(navController: NavHostController) {
val categorieViewModel: CategorieViewModel = viewModel()
NavHost(navController = navController, startDestination = "home") {
...
composable("listeCategories") {
ListeCategories( categorieViewModel, ...)
}
}
}
Une autre technique consiste à déclarer le ViewModel au même endroit que le navController et à le passer en paramètre au NavigationHost.
@Composable
fun MainScreen() {
val navController = rememberNavController()
val categorieViewModel: CategorieViewModel = viewModel()
Scaffold(
...,
content = {
...
NavigationHost(navController, categorieViewModel)
}
)
}
@Composable
fun NavigationHost(navController: NavHostController, categorieViewModel: CategorieViewModel ) {
NavHost(navController = navController, startDestination = "home") {
...
composable("listeCategories") {
ListeCategories( categorieViewModel , ...)
}
}
}
BottomAppBar¶
La classe BottomAppBar permet de définir ce qui apparaîtra dans le bas de l'écran.
La barre de navigation ainsi obtenue est très versatile.
Typiquement, on y ajoutera des icônes ou du texte pour effectuer des tâches ou pour atteindre différents écrans de l'application.
val navController = rememberNavController()
Scaffold(
...
bottomBar = {
BottomAppBar() {
IconButton(onClick = {
navController.navigate("home")
}) {
Icon(Icons.Filled.Home, contentDescription = "Accueil")
}
...
}
}
) {
...
}
Et voici le résultat.
Notez que l'espacement entre les icônes doit être effectué manuellement.

Voici un exemple de barre de navigation qui utilise des Button plutôt que des icônes.

Pour plus d'information¶
## NavigationBar
Lorsqu'une application Android avec Jetpack Compose comprend de 3 à 5 icônes de navigation, il est possible d'utiliser un NavigationBar plutôt que de styliser manuellement les liens de navigation.
Cette limite du nombre d'icônes provient de la documentation du NavigationBar :
NavigationBar should contain three to five NavigationBarItems, each representing a singular destination.
Si votre application ne répond pas à cette exigence, vous devrez configurer la barre de navigation avec BottomAppBar.
Voici un exemple d'application qui utilise un NavigationBar pour afficher trois icônes dans sa barre de navigation.
currentBackStackEntryAsState() expose l'entrée courante de la pile de navigation sous forme d'un état observable par Compose. Quand la destination change, Compose recalcule le composable et met à jour l'élément sélectionné.
val navController = rememberNavController()
val currentBackStackEntryState = navController.currentBackStackEntryAsState()
val currentRoute:String? = currentBackStackEntryState.value?.destination.route
Scaffold(
...
bottomBar = {
NavigationBar() {
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.Home,
contentDescription = "Accueil"
)
},
label = {
Text("Accueil")
},
selected = currentRoute == "home",
onClick = {
navController.navigate("home")
}
)
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.Info,
contentDescription = "Information"
)
},
label = {
Text("Information")
},
selected = currentRoute == "information",
onClick = {
navController.navigate("information")
}
)
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.Person,
contentDescription = "Mon compte"
)
},
label = {
Text("Mon compte")
},
selected = currentRoute == "compte",
onClick = {
navController.navigate("compte")
}
)
}
}
) {
...
}
Voici la barre de navigation obtenue.
Remarquez que les icônes sont automatiquement espacés pour prendre toute la largeur de l'écran.
De plus, un indicatif visuel marque l'icône qui correspond à la page active.

Source :
topBar et navigation¶
Le topBar du Scaffold peut être utilisé pour afficher un titre (ou sous-titre) en lien avec la page active en accord avec le currentRoute.