Le composant Navigation est compatible avec les applications Jetpack Compose. Vous pouvez naviguer entre les composables tout en tirant parti de l'infrastructure et des fonctionnalités du composant Navigation.
Configuration
Pour assurer la prise en charge de Compose, utilisez la dépendance suivante dans le fichier build.gradle
de votre module d'application:
Groovy
dependencies { def nav_version = "2.8.5" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.8.5" implementation("androidx.navigation:navigation-compose:$nav_version") }
Premiers pas
Lorsque vous implémentez la navigation dans une application, implémentez un hôte, un graphique et un contrôleur de navigation. Pour en savoir plus, consultez la présentation de la navigation.
Créer navController
Pour savoir comment créer un NavController
dans Compose, consultez la section Compose de la page Créer un contrôleur de navigation.
Créer un composable NavHost
Pour savoir comment créer un NavHost
dans Compose, consultez la section Compose de Concevoir votre graphique de navigation.
Accéder à un composable
Pour en savoir plus sur l'accès à un composable, consultez la section Accéder à une destination dans la documentation sur l'architecture.
Naviguer avec des arguments
Pour savoir comment transmettre des arguments entre des destinations composables, consultez la section Compose de Concevoir votre graphique de navigation.
Récupération de données complexes lors de la navigation
Lorsque vous effectuez des actions de navigation, nous vous recommandons vivement de ne pas transmettre d'objets de données complexes, mais plutôt de transmettre le strict minimum (comme un identifiant unique ou une autre forme d'ID) sous la forme d'arguments.
// Pass only the user ID when navigating to a new destination as argument
navController.navigate(Profile(id = "user1234"))
Les objets complexes doivent être stockés sous forme de données dans une référence unique, telle que la couche de données. Une fois arrivé à destination après votre navigation, vous pouvez charger les informations requises à partir de la référence unique en utilisant l'ID transmis. Pour récupérer les arguments de votre ViewModel
responsables de l'accès à la couche de données, utilisez la méthode SavedStateHandle
de ViewModel
:
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val profile = savedStateHandle.toRoute<Profile>()
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(profile.id)
// …
}
Cette approche permet de conserver les données lors des modifications de configuration et d'éviter les incohérences lors de la mise à jour ou de la mutation d'un objet.
Pour découvrir pourquoi vous devez éviter de transmettre des données complexes en tant qu'arguments, et pour obtenir la liste des types d'arguments compatibles, consultez la page Transmettre des données entre les destinations.
Liens profonds
Navigation Compose accepte également les liens profonds qui peuvent également être définis dans la fonction composable()
. Son paramètre deepLinks
accepte une liste d'objets NavDeepLink
, qui peuvent être créés facilement à l'aide de la méthode navDeepLink()
:
@Serializable data class Profile(val id: String)
val uri = "https://www.example.com"
composable<Profile>(
deepLinks = listOf(
navDeepLink<Profile>(basePath = "$uri/profile")
)
) { backStackEntry ->
ProfileScreen(id = backStackEntry.toRoute<Profile>().id)
}
Ces liens profonds vous permettent d'associer une URL, une action ou un type MIME spécifiques à un composable. Par défaut, ces liens profonds ne sont pas exposés à des applications externes. Pour rendre ces liens profonds disponibles en externe, vous devez ajouter les éléments <intent-filter>
appropriés au fichier manifest.xml
de votre application. Pour activer le lien profond dans l'exemple précédent, vous devez ajouter ce qui suit dans l'élément <activity>
du fichier manifeste:
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
La navigation crée automatiquement un lien profond dans ce composable lorsque le lien profond est déclenché par une autre application.
Ces mêmes liens profonds peuvent également être utilisés pour créer un PendingIntent
avec le lien profond approprié à partir d'un composable :
val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://www.example.com/profile/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
Vous pouvez ensuite utiliser ce deepLinkPendingIntent
comme n'importe quel autre PendingIntent
afin d'ouvrir votre application à l'emplacement de destination du lien profond.
Navigation imbriquée
Pour savoir comment créer des graphiques de navigation imbriqués, consultez la section Graphiques imbriqués.
Intégration avec la barre de navigation inférieure
En définissant NavController
à un niveau supérieur dans votre hiérarchie de composables, vous pouvez connecter Navigation à d'autres composants, comme le composant de navigation en bas de l'écran. Vous pouvez alors naviguer en sélectionnant les icônes dans la barre inférieure.
Pour utiliser les composants BottomNavigation
et BottomNavigationItem
, ajoutez la dépendance androidx.compose.material
à votre application Android.
Groovy
dependencies { implementation "androidx.compose.material:material:1.7.6" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.7.6") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Pour associer les éléments d'une barre de navigation inférieure aux itinéraires de votre graphique de navigation, nous vous recommandons de définir une classe, comme TopLevelRoute
ici, qui comporte une classe d'itinéraire et une icône.
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
Placez ensuite ces routes dans une liste pouvant être utilisée par BottomNavigationItem
:
val topLevelRoutes = listOf(
TopLevelRoute("Profile", Profile, Icons.Profile),
TopLevelRoute("Friends", Friends, Icons.Friends)
)
Dans votre composable BottomNavigation
, obtenez le NavBackStackEntry
actuel en utilisant la fonction currentBackStackEntryAsState()
. Cette entrée vous donne accès au NavDestination
actuel. L'état sélectionné de chaque BottomNavigationItem
peut ensuite être déterminé en comparant l'itinéraire de l'élément avec l'itinéraire de la destination actuelle et ses destinations parentes pour gérer les cas où vous utilisez la navigation imbriquée à l'aide de la hiérarchie NavDestination
.
L'itinéraire de l'élément est également utilisé pour connecter le lambda onClick
à un appel navigate
afin que l'utilisateur puisse appuyer sur l'élément pour y accéder. Grâce aux options saveState
et restoreState
, l'état et la pile "Retour" de cet élément sont correctement enregistrés et restaurés lorsque vous passez d'un élément de navigation à l'autre en bas de l'écran.
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
topLevelRoutes.forEach { topLevelRoute ->
BottomNavigationItem(
icon = { Icon(topLevelRoute.icon, contentDescription = topLevelRoute.name) },
label = { Text(topLevelRoute.name) },
selected = currentDestination?.hierarchy?.any { it.hasRoute(topLevelRoute.route::class) } == true,
onClick = {
navController.navigate(topLevelRoute.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(navController, startDestination = Profile, Modifier.padding(innerPadding)) {
composable<Profile> { ProfileScreen(...) }
composable<Friends> { FriendsScreen(...) }
}
}
Ici, vous allez exploiter la méthode NavController.currentBackStackEntryAsState()
pour hisser l'état navController
de la fonction NavHost
, puis le partager avec le composant BottomNavigation
. Cela signifie que BottomNavigation
possède automatiquement l'état le plus récemment mis à jour.
Interopérabilité
Si vous souhaitez utiliser le composant Navigation avec Compose, deux options s'offrent à vous :
- Définissez un graphique de navigation avec le composant Navigation pour les fragments.
- Définissez un graphique de navigation avec un
NavHost
dans Compose en utilisant les destinations Compose. Cela n'est possible que si tous les écrans du graphique de navigation sont des composables.
Pour les applications qui utilisent à la fois les vues et Compose, nous vous recommandons donc d'utiliser le composant Navigation basé sur des fragments. Les fragments contiendront ainsi les écrans basés sur les vues, les écrans Compose et les écrans qui utilisent à la fois les vues et Compose. Une fois que le contenu de chaque fragment se trouve dans Compose, l'étape suivante consiste à lier tous ces écrans avec Navigation Compose et à supprimer tous les fragments.
La navigation Compose avec les fragments
Pour modifier des destinations dans le code Compose, vous exposez des événements pouvant être transmis à n'importe quel composable de la hiérarchie et déclenchés par ceux-ci :
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
Dans votre fragment, vous créez le pont entre Compose et le composant de navigation basé sur des fragments en recherchant NavController
et en accédant à la destination :
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
Vous pouvez également transmettre l'élément NavController
à votre hiérarchie Compose.
Toutefois, l'exposition de fonctions simples est beaucoup plus réutilisable et testable.
Tests
Dissociez le code de navigation de vos destinations de composables afin de pouvoir tester chaque composable séparément, indépendamment du composable NavHost
.
Cela signifie que vous ne devez pas transmettre navController
directement dans un composable, mais transmettre des rappels de navigation sous la forme de paramètres. Tous vos composables peuvent ainsi être testés individuellement, car ils ne nécessitent pas d'instance de navController
lors des tests.
C'est le niveau d'indirection fourni par le lambda composable
qui vous permet de séparer votre code de navigation du composable lui-même. Cela fonctionne dans deux sens :
- transmettre des arguments analysés dans votre composable uniquement ;
- transmettre des lambdas qui doivent être déclenchés par le composable pour naviguer, plutôt que le
NavController
lui-même.
Par exemple, un composable ProfileScreen
qui accepte une entrée userId
et permet aux utilisateurs d'accéder à la page de profil d'un ami peut avoir la signature suivante:
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
De cette façon, le composable ProfileScreen
fonctionne indépendamment de la navigation, ce qui lui permet d'être testé séparément. Le lambda composable
encapsule la logique minimale requise pour combler l'écart entre les API de navigation et votre composable :
@Serializable data class Profile(id: String)
composable<Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(userId = profile.id) { friendUserId ->
navController.navigate(route = Profile(id = friendUserId))
}
}
Nous vous recommandons d'écrire des tests qui couvrent les besoins de votre application en matière de navigation. Pour ce faire, testez le NavHost
, les actions de navigation transmises à vos composables ainsi qu'à vos composables d'écran individuels.
Tester le NavHost
Pour commencer à tester votre NavHost
, ajoutez la dépendance navigation-testing suivante:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
Encapsulez le NavHost
de votre application dans un composable qui accepte un NavHostController
comme paramètre.
@Composable
fun AppNavHost(navController: NavHostController){
NavHost(navController = navController){ ... }
}
Vous pouvez maintenant tester AppNavHost
et toute la logique de navigation définie dans NavHost
en transmettant une instance de l'artefact de test de navigation TestNavHostController
. Voici à quoi ressemble un test d'UI qui vérifie la destination de départ de votre application et le NavHost
:
class NavigationTest {
@get:Rule
val composeTestRule = createComposeRule()
lateinit var navController: TestNavHostController
@Before
fun setupAppNavHost() {
composeTestRule.setContent {
navController = TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(ComposeNavigator())
AppNavHost(navController = navController)
}
}
// Unit test
@Test
fun appNavHost_verifyStartDestination() {
composeTestRule
.onNodeWithContentDescription("Start Screen")
.assertIsDisplayed()
}
}
Tester les actions de navigation
Vous pouvez tester l'implémentation de la navigation de différentes manières : en cliquant sur les éléments de l'UI, puis en vérifiant la destination affichée ou en comparant l'itinéraire attendu par rapport à l'itinéraire actuel.
Pour tester l'implémentation concrète de votre application, il est préférable de tester les clics sur les éléments de l'UI. Pour découvrir comment procéder de manière isolée avec des fonctions modulables individuelles, consultez l'atelier de programmation Tests dans Jetpack Compose.
Vous pouvez également utiliser navController
pour vérifier vos assertions en comparant l'itinéraire actuel à celui attendu, à l'aide du currentBackStackEntry
de navController
:
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
Pour en savoir plus sur les principes de base des tests Compose, consultez Tester votre mise en page Compose et l'atelier de programmation Tester dans Jetpack Compose. Pour en savoir plus sur les tests avancés du code de navigation, consultez le guide intitulé Tester la navigation.
En savoir plus
Pour en savoir plus sur le composant Navigation de Jetpack, consultez Premiers pas avec le composant Navigation ou suivez l'atelier de programmation sur la navigation Jetpack Compose.
Pour découvrir comment concevoir la navigation de votre application pour l'adapter à différentes tailles d'écran, orientations et facteurs de forme, consultez la page Navigation pour les interfaces utilisateur responsives.
Pour en savoir plus sur l'implémentation plus avancée de la navigation Compose dans une application modularisée, y compris des concepts tels que les graphiques imbriqués et l'intégration de la barre de navigation inférieure, consultez l'application Now in Android sur GitHub.
Exemples
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Material Design 2 dans Compose
- Migrer Jetpack Navigation vers Navigation Compose
- Où hisser l'état