Aller au contenu

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

Affichage de la page actuelle

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 :

Fichier app/build.gradle.kts
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.

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.

Jetpack Compose (Kotlin)
import androidx.navigation.compose.rememberNavController
...
val navController = rememberNavController()
Scaffold(
    ...
)

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.

Fichier NavigationHost.kt
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.

Fichier ui/HomeScreen.kt
@Composable
fun HomeScreen(...) {
    ...
}
Fichier ui/PageUn.kt
@Composable
fun PageUn(...) {
    ...
}
Fichier ui/PageDeux.kt
@Composable
fun PageDeux(...) {
    ...
}

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.

Jetpack Compose (Kotlin)
val navController = rememberNavController()
Scaffold(
    ...
) {
    // contenu du Scaffold
    Column(
        modifier = Modifier.padding(it)
    ) {
         NavigationHost(navController = navController)
    }
}

La méthode navController.navigate permet d'atteindre la page souhaitée et de l'ajouter à la pile des pages affichées.

Jetpack Compose (Kotlin)
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().

Jetpack Compose (Kotlin)
navController.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:

Fichier NavigationHost.kt
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.

Jetpack Compose (Kotlin)
@Composable
fun RechercherItem(navController: NavController, texte: String) {
    ...
}

Pour naviguer vers une route avec paramètre :

Jetpack Compose (Kotlin)
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.:

Fichier NavigationHost.kt
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 :

Jetpack Compose (Kotlin)
navController.navigate("editerItem/2")

Route avec paramètres optionnels

Si le paramètre est optionnel :

Fichier NavigationHost.kt
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).

Jetpack Compose (Kotlin)
@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.

Jetpack Compose (Kotlin)
navController.navigate("detailsItem?itemId=3")
...
// Page de détails vide
navController.navigate("detailsItem")

Pour plus d'information

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.

Fichier NavigationHost.kt
@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.

Jetpack Compose(Kotlin)
@Composable
fun MainScreen() {
    val navController = rememberNavController()
    val categorieViewModel: CategorieViewModel = viewModel()
    Scaffold(
        ...,
        content = {
            ...
            NavigationHost(navController, categorieViewModel)
        }
    )
}
Fichier NavigationHost.kt
@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.

Jetpack Compose (Kotlin)
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.

Illustration

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

Illustration

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é.

Jetpack Compose (Kotlin)
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.

Illustration

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.

Jetpack Compose (Kotlin)
Scaffold(
    topBar = {
        TopAppBar(
            title = {
                Text(
                    when (currentRoute) {
                        "home" -> "Accueil"
                        "information" -> "Information"
                        "compte" -> "Mon compte"
                        else -> ""
                    }
                )
            }
        )        
    },
    // ... le reste du Scaffold
)