Pour migrer votre application de Navigation 2 vers Navigation 3, procédez comme suit :
- Ajoutez les dépendances de Navigation 3.
- Mettez à jour vos itinéraires de navigation pour implémenter l'interface
NavKey. - Créez des classes pour contenir et modifier votre état de navigation.
- Remplacez
NavControllerpar ces classes. - Déplacez vos destinations de
NavGraphdeNavHostvers unentryProvider. - Remplacez
NavHostparNavDisplay. - Supprimez les dépendances de Navigation 2.
Prompt d'IA
Migrer de Navigation 2 vers Navigation 3
Ce prompt utilisera ce guide pour migrer vers Navigation 3.
Migrate from Navigation 2 to Navigation 3 using the official
migration guide.Si vous rencontrez des problèmes, signalez-les ici.
Préparation
Les sections suivantes décrivent les prérequis pour la migration et les hypothèses concernant votre projet. Elles couvrent également les fonctionnalités compatibles avec la migration et celles qui ne le sont pas.
Prérequis
- Vous devez utiliser un
compileSdkde 36 ou version ultérieure. - Vous devez connaître la terminologie de navigation.
- Les destinations sont des fonctions modulables. Navigation 3 est conçu exclusivement pour Compose. Pour utiliser des fragments et des vues dans Compose, consultez Utiliser des vues dans Compose.
- Les itinéraires sont fortement typés. Si vous utilisez des itinéraires basés sur des chaînes, migrez vers des itinéraires de type sécurisé d'abord (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 concernant vous et votre projet :
- Vous disposez d'un ou de plusieurs itinéraires de premier niveau (généralement affichés dans une barre de navigation inférieure), et chacun possède 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 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émentale 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 fonctions modulables
- 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 les suivants :
- Feuilles inférieures (instructions fournies dans ce guide)
- Code de navigation modularisé et destinations injectées
- Utiliser et transmettre des arguments à
ViewModel - Renvoi de résultats à partir d'un écran
Si votre projet comporte l'une de ces fonctionnalités, consultez la recette correspondante pour comprendre comment les implémenter.
Agent IA : Avant de modifier le code, vérifiez si le projet contient des fonctionnalités compatibles via des 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 avoir confirmé 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. Elles ne sont tout simplement pas abordées ici.
- Plus d'un niveau de navigation imbriquée
- Destinations partagées : écrans pouvant passer d'une pile "Retour" à une autre
- Types de destinations personnalisés
- Liens profonds
Agent IA : Avant de modifier le code, vérifiez si le projet contient l'une 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 de Navigation 3 à votre projet. Les dépendances principales sont fournies pour que vous puissiez les copier.
lib.versions.toml
[versions]
nav3Core = "1.0.0"
# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.10.0-rc01"
[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)
}
Mettez également à jour le minSdk du projet sur 23 et le compileSdk sur 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 itinéraire de navigation route afin qu'il implémente l'NavKey
interface. Cela vous permet d'utiliser rememberNavBackStack pour vous aider à enregistrer votre
état de navigation.
Avant :
@Serializable data object RouteA
Après :
@Serializable data object RouteA : NavKey
Étape 3 : Créez des classes pour contenir et modifier votre état de navigation
Étape 3.1 : Créez 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 des fonctions d'assistance associées. Il contient un ensemble d'itinéraires de premier niveau, chacun avec sa propre pile "Retour". En interne, il utilise rememberSerializable (et non rememberSaveable) pour conserver l'itinéraire de premier niveau actuel et rememberNavBackStack pour conserver les piles "Retour" de chaque itinéraire de premier niveau.
Étape 3.2 : Créez un objet qui modifie l'état de navigation en réponse à des é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 :
navigatevers un itinéraire spécifique.goBackà partir de l'itinéraire actuel.
Les deux méthodes modifient le NavigationState.
Étape 3.3 : Créez les NavigationState et Navigator
Créez des instances de NavigationState et Navigator avec la même portée 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 des équivalents Navigator.
Champ ou méthode |
Équivalent |
|---|---|
|
|
|
|
Remplacez les champs NavController par des champs NavigationState.
Champ ou méthode |
Équivalent |
|---|---|
|
|
|
|
Obtenez l'itinéraire de premier niveau : parcourez la hiérarchie à partir de l'entrée de la pile "Retour" actuelle pour le 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 : Migrez la logique sensible au 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
à portée d'entrée via LocalLifecycleOwner.current au contenu modulable
de chaque destination. Pour en savoir plus, consultez Cycle de vie des destinations.
Vous devez effectuer des opérations sensibles au cycle de vie directement dans le contenu modulable de votre destination en référençant LocalLifecycleOwner.current.
Par exemple, si vous collectez un flux de manière sensible au 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 5 : Déplacez vos destinations de 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 un itinéraire en 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 d'itinéraires de premier niveau (les itinéraires parents) et une pile pour chacun d'eux. Il suit l'itinéraire de premier niveau actuel et sa pile associée.- Lorsque vous accédez à un nouvel itinéraire,
Navigatorvérifie s'il s'agit d'un itinéraire de premier niveau. Si c'est le cas, l'itinéraire et la pile de premier niveau actuels sont mis à jour. Sinon, il s'agit d'un itinéraire enfant qui est ajouté à la pile actuelle.
Étape 5.1 : Créez un entryProvider
Créez un entryProvider à l'aide du DSL avec la même portée que le
NavigationState.
val entryProvider = entryProvider {
}
Étape 5.2 : Déplacez les destinations vers le entryProvider
Pour chaque destination définie dans NavHost, procédez comme suit en fonction du type de destination :
navigation: supprimez-le avec l'itinéraire. Il n'est pas nécessaire d'avoir des "itinéraires de base", car les itinéraires de premier niveau peuvent identifier chaque pile "Retour" imbriquée.composable<T>: déplacez-le versentryProvideret renommez-le enentry, 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 du bottom sheet ici. Cela ressemble aux instructions pourdialog, sauf queBottomSheetSceneStrategyne fait pas partie de la bibliothèque principale Navigation 3. Vous devez donc la copier dans votre projet.
Agent IA : Lorsque vous supprimez des itinéraires utilisés pour identifier un graphique imbriqué, remplacez toutes les
références à l'itinéraire supprimé 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 à celui-ci par RouteA. Ce remplacement doit généralement être effectué pour la liste fournie à une barre de navigation, un rail ou un panneau.
Vous pouvez refactoriser NavGraphBuilder fonctions d'extension en
EntryProviderScope<T> fonctions d'extension, puis les déplacer.
Obtenez les arguments de navigation à l'aide de la clé fournie au lambda final 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
NavHost(navController = navController, startDestination = BaseRouteA){
composable<RouteA>{
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. - Connectez
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 :
import androidx.navigation3.ui.NavDisplay
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategies = remember { listOf(DialogSceneStrategy()) }
)
Étape 7 : Supprimez les dépendances de Navigation 2
Supprimez toutes les importations et dépendances de bibliothèque de Navigation 2.
Résumé
Félicitations ! Votre projet est désormais migré vers Navigation 3. Si vous ou votre agent IA avez rencontré des problèmes lors de l'utilisation de ce guide, signalez un bug ici.