Compétences Android
Afficher sur GitHubJetpack Navigation 3
android skills add navigation-3Pour migrer votre application de Navigation 2 vers Navigation 3, procédez comme suit :
- Ajoutez les dépendances Navigation 3.
- Mettez à jour vos itinéraires de navigation pour implémenter l'interface
NavKey. - Créer des classes pour stocker et modifier l'état de navigation
- Remplacez
NavControllerpar ces classes. - Déplacez vos destinations de la
NavGraphdeNavHostvers unentryProvider. - Remplacez
NavHostparNavDisplay. - Supprimez les dépendances Navigation 2.
Préparation
Les sections suivantes décrivent les conditions préalables à la migration et les hypothèses concernant votre projet. Ils couvrent également les fonctionnalités compatibles avec la migration et celles qui ne le sont pas.
Prérequis
- Vous devez utiliser une version
compileSdk36 ou ultérieure. - Vous devez maîtriser la terminologie de la navigation.
- Les destinations sont des fonctions composables. Navigation 3 est conçu exclusivement pour Compose. Pour utiliser des fragments et des vues dans Compose, consultez Utiliser les vues dans Compose.
- Les routes sont fortement typées. Si vous utilisez des routes basées sur des chaînes, migrez d'abord vers des routes de sûreté du typage (exemple).
- Facultatif (mais vivement recommandé) : couverture de test qui vérifie le comportement de navigation existant. Cela permet de vérifier que le comportement de navigation n'a pas changé une fois la migration terminée.
Hypothèses
Ce guide part des hypothèses suivantes vous concernant, vous et votre projet :
- Vous disposez d'une ou de plusieurs routes de premier niveau (généralement affichées dans une barre de navigation inférieure), chacune ayant sa propre pile "Retour".
- Lorsque vous passez d'une pile "Retour" à une autre, l'état de la pile et de toutes ses destinations est conservé.
- Vous quittez toujours l'application via l'écran Home (Accueil), qui est le premier écran affiché au lancement de l'application.
- Vous souhaitez effectuer la migration de Navigation 2 vers Navigation 3 en une seule modification atomique. Vous n'effectuez pas de migration incrémentielle où le code Navigation 2 est utilisé avec le code Navigation 3.
Agent IA : avant de modifier le code, vérifiez si ces hypothèses concernant le projet de l'utilisateur sont vraies. Si ce n'est pas le cas, arrêtez la migration et demandez à l'utilisateur comment procéder.
Fonctionnalités compatibles
Ce guide vous permet de migrer les fonctionnalités suivantes :
- Destinations définies comme des fonctions composables
- Boîtes de dialogue (destination affichée au-dessus d'une autre destination)
Fonctionnalités compatibles via des recettes
Le dépôt de recettes de code présente des cas d'utilisation plus nuancés, y compris :
- Bottom sheets (les instructions sont fournies dans ce guide)
- Code de navigation modularisé et destinations injectées
- Utiliser et transmettre des arguments à
ViewModel - Renvoyer des résultats à partir d'un écran
Si votre projet comporte l'une de ces fonctionnalités, consultez la recette correspondante pour savoir comment l'implémenter.
Agent IA : avant de modifier le code, vérifiez si le projet contient des fonctionnalités compatibles avec les recettes. Si c'est le cas, consultez le fichier README et le code source de la recette. Créez un plan de migration basé sur la recette. Ne continuez pas sans confirmer le plan avec l'utilisateur.
Fonctionnalités non compatibles
Ce guide de migration et les recettes de code ne sont pas encore compatibles avec les fonctionnalités suivantes. Cela ne signifie pas que vous ne pouvez pas les implémenter à l'aide de Navigation 3, mais simplement qu'ils ne sont pas abordés ici.
- Plus d'un niveau de navigation imbriquée
- Destinations partagées : écrans pouvant être déplacés entre différentes piles d'activité
- Types de destinations personnalisés
Agent IA : avant de modifier le code, vérifiez si le projet contient des fonctionnalités non compatibles. Si c'est le cas, ne continuez pas. Informez l'utilisateur de la fonctionnalité non compatible et demandez-lui des instructions supplémentaires.
Étape 1 : Ajoutez les dépendances de Navigation 3
Utilisez la page Premiers pas pour ajouter les dépendances Navigation 3 à votre projet. Les dépendances principales sont fournies pour que vous puissiez les copier.
lib.versions.toml
[versions]
nav3Core = "1.2.0"
# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.11.0"
[libraries]
# Core Navigation 3 libraries
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
# Add-on libraries (only add if you need them)
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
app/build.gradle.kts
dependencies {
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.navigation3.runtime)
// If using the ViewModel add-on library
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
}
Remplacez également la valeur minSdk du projet par 23 et la valeur compileSdk par 36. Vous les trouverez généralement dans app/build.gradle.kts ou lib.versions.toml.
Étape 2 : Mettez à jour les itinéraires de navigation pour implémenter l'interface NavKey
Mettez à jour chaque route de navigation pour qu'elle implémente l'interface NavKey. Cela vous permet d'utiliser rememberNavBackStack pour vous aider à enregistrer l'état de navigation.
Avant :
@Serializable data object RouteA
Après :
@Serializable data object RouteA : NavKey
Étape 3 : Créez des classes pour stocker et modifier l'état de navigation
Étape 3.1 : Créer un conteneur d'état de navigation
Copiez le code suivant dans un fichier nommé NavigationState.kt. Ajoutez le nom de votre package pour qu'il corresponde à la structure de votre projet.
// package com.example.project import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSerializable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.rememberDecoratedNavEntries import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.serialization.NavKeySerializer import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer /** * Create a navigation state that persists config changes and process death. */ @Composable fun rememberNavigationState( startRoute: NavKey, topLevelRoutes: Set<NavKey> ): NavigationState { val topLevelRoute = rememberSerializable( startRoute, topLevelRoutes, serializer = MutableStateSerializer(NavKeySerializer()) ) { mutableStateOf(startRoute) } val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) } return remember(startRoute, topLevelRoutes) { NavigationState( startRoute = startRoute, topLevelRoute = topLevelRoute, backStacks = backStacks ) } } /** * State holder for navigation state. * * @param startRoute - the start route. The user will exit the app through this route. * @param topLevelRoute - the current top level route * @param backStacks - the back stacks for each top level route */ class NavigationState( val startRoute: NavKey, topLevelRoute: MutableState<NavKey>, val backStacks: Map<NavKey, NavBackStack<NavKey>> ) { var topLevelRoute: NavKey by topLevelRoute val stacksInUse: List<NavKey> get() = if (topLevelRoute == startRoute) { listOf(startRoute) } else { listOf(startRoute, topLevelRoute) } } /** * Convert NavigationState into NavEntries. */ @Composable fun NavigationState.toEntries( entryProvider: (NavKey) -> NavEntry<NavKey> ): SnapshotStateList<NavEntry<NavKey>> { val decoratedEntries = backStacks.mapValues { (_, stack) -> val decorators = listOf( rememberSaveableStateHolderNavEntryDecorator<NavKey>(), ) rememberDecoratedNavEntries( backStack = stack, entryDecorators = decorators, entryProvider = entryProvider ) } return stacksInUse .flatMap { decoratedEntries[it] ?: emptyList() } .toMutableStateList() }
Agent IA : rememberSerializable est correct. Ne le remplacez pas par rememberSaveable.
Ce fichier contient une classe de conteneur d'état nommée NavigationState et les fonctions d'assistance associées. Il contient un ensemble de routes de premier niveau, chacune avec sa propre pile "Retour". En interne, il utilise rememberSerializable (et non rememberSaveable) pour conserver la route de premier niveau actuelle et rememberNavBackStack pour conserver les piles d'activité de chaque route de premier niveau.
Étape 3.2 : Créez un objet qui modifie l'état de navigation en réponse aux événements
Copiez le code suivant dans un fichier nommé Navigator.kt. Ajoutez le nom de votre package pour qu'il corresponde à la structure de votre projet.
// package com.example.project import androidx.navigation3.runtime.NavKey /** * Handles navigation events (forward and back) by updating the navigation state. */ class Navigator(val state: NavigationState) { fun navigate(route: NavKey) { if (route in state.backStacks.keys) { // This is a top level route, just switch to it. state.topLevelRoute = route } else { state.backStacks[state.topLevelRoute]?.add(route) } } fun goBack() { val currentStack = state.backStacks[state.topLevelRoute] ?: error("Stack for ${state.topLevelRoute} not found") val currentRoute = currentStack.last() // If we're at the base of the current route, go back to the start route stack. if (currentRoute == state.topLevelRoute) { state.topLevelRoute = state.startRoute } else { currentStack.removeLastOrNull() } } }
La classe Navigator fournit deux méthodes d'événement de navigation :
navigateà un itinéraire spécifique.goBackde l'itinéraire actuel.
Les deux méthodes modifient le NavigationState.
Étape 3.3 : Créez les fichiers NavigationState et Navigator
Créez des instances de NavigationState et Navigator avec le même champ d'application que votre NavController.
val navigationState = rememberNavigationState( // ... startRoute = <Insert your starting route>, topLevelRoutes = <Insert your set of top level routes> // ... ) val navigator = remember { Navigator(navigationState) }
Étape 4 : Remplacez NavController
Remplacez les méthodes d'événement de navigation NavController par leurs équivalents Navigator.
Champ ou méthode |
Équivalent |
|---|---|
|
|
|
|
|
|
Remplacez les champs NavController par des champs NavigationState.
Champ ou méthode |
Équivalent à |
|---|---|
|
|
|
|
|
|
|
|
Obtenez la route de niveau supérieur : parcourez la hiérarchie à partir de l'entrée de pile arrière actuelle pour la trouver. |
|
Utilisez NavigationState.topLevelRoute pour déterminer l'élément actuellement sélectionné dans une barre de navigation.
Avant :
// ... val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class) // ... fun NavDestination?.isRouteInHierarchy(route: KClass<*>) = this?.hierarchy?.any { it.hasRoute(route) } ?: false
Après :
val isSelected = key == navigationState.topLevelRoute
Vérifiez que vous avez supprimé toutes les références à NavController, y compris les importations.
Étape 4.1 : Migrer la logique tenant compte du cycle de vie
Dans Navigation 2, NavBackStackEntry implémente LifecycleOwner, ce qui vous permet d'écouter les événements de cycle de vie ou de collecter des flux de manière sensible au cycle de vie à l'aide de navController.currentBackStackEntry.
Dans Navigation 3, NavDisplay fournit un LifecycleOwner limité à l'entrée via LocalLifecycleOwner.current au contenu composable de chaque destination. Pour en savoir plus, consultez Cycle de vie de la destination.
Vous devez effectuer des opérations tenant compte du cycle de vie directement dans le contenu composable de votre destination en référençant LocalLifecycleOwner.current.
Par exemple, si vous collectez un flux en tenant compte du cycle de vie à l'aide de l'entrée de la pile "Retour" :
Avant :
// In your destination screen or host val lifecycleOwner = navController.currentBackStackEntry!! val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
Après :
// Inside the destination composable val state by flow.collectAsStateWithLifecycle()
Étape 4.2 : Migrer la transmission des résultats
Dans Navigation 2, les destinations renvoyaient les résultats aux destinations précédentes à l'aide de SavedStateHandle sur NavBackStackEntry. Comme SavedStateHandle est sauvegardé par l'état d'instance enregistré, les données renvoyées ont survécu automatiquement à l'arrêt du processus.
Avant :
// Sender destination: navController.previousBackStackEntry?.savedStateHandle?.set("contact_key", contact) navController.popBackStack() // Receiver destination: val lifecycleOwner = LocalLifecycleOwner.current navController.currentBackStackEntry?.savedStateHandle ?.getLiveData<Contact>("contact_key") ?.observe(lifecycleOwner) { contact -> viewModel.onRecipientSelected(contact) }
Dans Navigation 3, ajoutez rememberResultEventBusNavEntryDecorator() à votre NavDisplay.entryDecorators. Dans le mappage entryProvider de votre destination d'expéditeur, récupérez LocalResultEventBus.current et appelez sendResult(). Dans la destination de réception, utilisez ResultEffect pour transférer l'événement vers un ViewModel ou déclencher un effet secondaire.
Après :
// Sender destination (in entryProvider): entry<ContactPickerRoute> { val resultBus = LocalResultEventBus.current ContactPickerScreen( onContactSelected = { contact -> resultBus.sendResult<Contact>(result = contact) navigator.goBack() } ) } // Receiver destination: @Composable fun ComposeMessageScreen(viewModel: ComposeMessageViewModel = viewModel()) { ResultEffect<Contact> { contact -> viewModel.onRecipientSelected(contact) } ComposeMessageContent(recipient = viewModel.recipient) }
Pour l'observation basée sur l'état, vous pouvez appeler resultBus.conflateAsState(). Pour en savoir plus, consultez Renvoyer les résultats.
Étape 5 : Déplacez vos destinations de l'NavGraph de NavHost vers un entryProvider
Dans Navigation 2, vous définissez vos destinations à l'aide du DSL NavGraphBuilder, généralement dans le lambda final de NavHost. Il est courant d'utiliser des fonctions d'extension ici, comme décrit dans Encapsuler votre code de navigation.
Dans Navigation 3, vous définissez vos destinations à l'aide d'un entryProvider. Ce entryProvider résout une route vers un NavEntry. Il est important de noter que entryProvider ne définit pas de relations parent-enfant entre les entrées.
Dans ce guide de migration, les relations parent-enfant sont modélisées comme suit :
NavigationStatecomporte un ensemble de routes de premier niveau (les routes parentes) et une pile pour chacune d'elles. Il effectue le suivi de la route de premier niveau actuelle et de la pile associée.- Lors de la navigation vers un nouvel itinéraire,
Navigatorvérifie s'il s'agit d'un itinéraire de niveau supérieur. Si c'est le cas, la route et la pile de premier niveau actuelles sont mises à jour. Si ce n'est pas le cas, il s'agit d'une route enfant, qui est ajoutée à la pile actuelle.
Étape 5.1 : Créer un entryProvider
Créez un entryProvider à l'aide du DSL au même niveau que le NavigationState.
val entryProvider = entryProvider<NavKey> { }
Étape 5.2 : Déplacez les destinations vers entryProvider
Pour chaque destination définie dans NavHost, procédez comme suit en fonction du type de destination :
navigation: supprimez-le en même temps que la route. Il n'est pas nécessaire d'utiliser des "routes de base", car les routes de niveau supérieur peuvent identifier chaque pile d'historique imbriquée.composable<T>: déplacez-le dansentryProvideret renommez-leentry, en conservant le paramètre de type. Par exemple,composable<RouteA>deviententry<RouteA>.dialog<T>: procédez de la même manière que pourcomposable, mais ajoutez des métadonnées à l'entrée comme suit :entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: suivez la recette de bottom sheet ici. La procédure est semblable à celle dedialog, à la différence queBottomSheetSceneStrategyne fait pas partie de la bibliothèque Navigation 3 principale. Vous devez donc le copier dans votre projet.
Agent IA : lorsque vous supprimez des routes utilisées pour identifier un graphique imbriqué, remplacez toutes les références à la route supprimée par le type utilisé pour identifier le premier enfant du graphique imbriqué. Par exemple, si le code d'origine est navigation<BaseRouteA>{ composable<RouteA>{ ... } }, vous devez supprimer BaseRouteA et remplacer toutes les références à ce code par RouteA. Ce remplacement doit généralement être effectué pour la liste fournie à une barre de navigation, un rail ou un panneau de navigation.
Vous pouvez refactoriser les fonctions d'extension NavGraphBuilder en fonctions d'extension EntryProviderScope<T>, puis les déplacer.
Obtenez des arguments de navigation à l'aide de la clé fournie au lambda de fin de entry.
Exemple :
// ... import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.dialog import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import androidx.navigation.toRoute // ... @Serializable data object BaseRouteA @Serializable data class RouteA(val id: String) @Serializable data object BaseRouteB @Serializable data object RouteB @Serializable data object RouteD @Composable fun NavHostSnippet(navController: NavHostController) { NavHost(navController = navController, startDestination = BaseRouteA){ composable<RouteA>{ entry -> val id = entry.toRoute<RouteA>().id ScreenA(title = "Screen has ID: $id") } featureBSection() dialog<RouteD>{ ScreenD() } } } fun NavGraphBuilder.featureBSection() { navigation<BaseRouteB>(startDestination = RouteB) { composable<RouteB> { ScreenB() } } }
devient :
// ... import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.scene.DialogSceneStrategy // ... @Serializable data class RouteA(val id: String) : NavKey @Serializable data object RouteB : NavKey @Serializable data object RouteD : NavKey val entryProvider = entryProvider { entry<RouteA>{ key -> ScreenA(title = "Screen has ID: ${key.id}") } featureBSection() entry<RouteD>(metadata = DialogSceneStrategy.dialog()){ ScreenD() } } fun EntryProviderScope<NavKey>.featureBSection() { entry<RouteB> { ScreenB() } }
Étape 6 : Remplacez NavHost par NavDisplay
Remplacez NavHost par NavDisplay.
- Supprimez
NavHostet remplacez-le parNavDisplay. - Spécifiez
entries = navigationState.toEntries(entryProvider)comme paramètre. Cela convertit l'état de navigation en entrées queNavDisplayaffiche à l'aide deentryProvider. - Connecter
NavDisplay.onBackànavigator.goBack(). Cela entraîne la mise à jour de l'état de navigation parnavigatorlorsque le gestionnaire de retour intégré deNavDisplayest terminé. - Si vous avez des destinations de boîte de dialogue, ajoutez
DialogSceneStrategyau paramètresceneStrategiesdeNavDisplay.
Exemple :
NavDisplay( entries = navigationState.toEntries(entryProvider), onBack = { navigator.goBack() }, sceneStrategies = remember { listOf(DialogSceneStrategy()) } )
Étape 7 : Migrer les liens profonds
Dans Navigation 2, les liens profonds étaient définis directement dans le graphique de navigation à l'aide du paramètre deepLinks des destinations.
Dans Navigation 3, les liens profonds sont gérés indépendamment de l'UI de navigation. Vous définissez des DeepLinkMatcher et faites correspondre les requêtes entrantes dans votre activité pour créer la pile "Retour" initiale.
Avant :
Dans Navigation 2, vous avez peut-être défini un lien profond comme suit :
composable<RouteA>( deepLinks = listOf( navDeepLink { uriPattern = "www.example.com/user/{id}" } ) ) { // ... }
Après :
Dans Navigation 3, vous définissez un UriDeepLinkMatcher pour l'itinéraire :
val userMatcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/user/{id}"), serializer<RouteA>() )
Ensuite, dans les méthodes onCreate (et onNewIntent) de votre activité, vous faites correspondre l'intent entrant et initialisez votre pile "Retour" :
val deepLinkMatchers: List<DeepLinkMatcher<*, *>> = listOf( userMatcher, ) class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val request = DeepLinkRequest(intent = intent) val matchResult = deepLinkMatchers .mapNotNull { it.match(request) } .maxOrNull() val backStack = when (matchResult) { null -> listOf(HomeKey) is BackStackMatchResult<*, *> -> { @Suppress("UNCHECKED_CAST") matchResult.backStack as List<NavKey> } else -> listOf(matchResult.key) } // Use backStack with NavDisplay } }
Types d'arguments personnalisés
Dans Navigation 2, vous gériez les types d'arguments personnalisés ou tiers (comme LocalDateTime) à l'aide d'implémentations NavType personnalisées et de typeMap.
Dans Navigation 3, définissez un DeepLinkSerializer pour désérialiser les types personnalisés ou tiers à partir des paramètres d'URI. Pour en savoir plus, consultez Sérialisation personnalisée avec DeepLinkSerializer.
Pour les cas d'utilisation plus avancés, y compris les piles « Retour » synthétiques et les outils de mise en correspondance personnalisés, consultez le guide Prendre en charge les liens profonds.
Étape 8 : Supprimez les dépendances Navigation 2
Supprimez toutes les importations et dépendances de bibliothèque de Navigation 2.
Résumé
Félicitations ! Votre projet est maintenant migré vers Navigation 3. Si vous ou votre agent IA rencontrez des problèmes en utilisant ce guide, signalez un bug ici.