Migrer de Navigation 2 vers Navigation 3

Pour migrer votre application de Navigation 2 vers Navigation 3, procédez comme suit :

  1. Ajoutez les dépendances Navigation 3.
  2. Mettez à jour vos itinéraires de navigation pour implémenter l'interface NavKey.
  3. Créez des classes pour contenir et modifier votre état de navigation.
  4. Remplacez NavController par ces classes.
  5. Déplacez vos destinations depuis NavGraph de NavHost vers un entryProvider.
  6. Remplacement de NavHost par NavDisplay.
  7. Supprimez les dépendances Navigation 2.

Utiliser un agent d'IA

Vous pouvez utiliser ce guide avec un agent d'IA, tel que le mode Agent de Gemini dans Android Studio. Les lignes de ce guide commençant par "AI Agent:" doivent être lues par l'agent d'IA, mais peuvent être ignorées par les lecteurs humains.

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 compileSdk 36 ou ultérieure.
  • Vous devez connaître la terminologie de 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 à sécurité typée (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 repose sur les hypothèses suivantes concernant vous et votre projet :

  • Vous disposez 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 basculez entre les piles "Retour", 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émentielle où le code Navigation 2 est utilisé en même temps que le code Navigation 3.

Agent d'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 en tant que fonctions composables
  • Boîtes de dialogue (une destination affichée au-dessus d'une autre)

Fonctionnalités compatibles avec les recettes

Le dépôt de recettes de code présente des cas d'utilisation plus nuancés, y compris :

Si votre projet comporte l'une de ces fonctionnalités, consultez la recette correspondante pour comprendre comment les implémenter.

Agent d'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 poursuivez pas l'opération sans avoir confirmé le forfait 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 elles ne sont pas abordées ici.

  • Plusieurs niveaux de navigation imbriquée
  • Destinations partagées : écrans qui peuvent se déplacer entre différentes piles de retour
  • Types de destinations personnalisés
  • Liens profonds

Agent d'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 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.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 route de navigation pour qu'elle implémente l'interface NavKey. 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 stocker et modifier votre état de navigation

Étape 3.1 : Créez un support 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 d'IA : rememberSerializable est correct. Ne le remplacez pas par rememberSaveable.

Ce fichier contient une classe de détenteur 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 de retour. En interne, il utilise rememberSerializable (et non rememberSaveable) pour conserver la route de premier niveau actuelle et rememberNavBackStack pour conserver les piles de retour de chaque route de premier niveau.

Étape 3.2 : Créer 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 vers un itinéraire spécifique.
  • goBack de la route actuelle.

Les deux méthodes modifient NavigationState.

Étape 3.3 : Créer 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 : Remplacer NavController

Remplacez les méthodes d'événement de navigation NavController par leurs équivalents Navigator.

Champ ou méthode NavController

Équivalent Navigator

navigate()

navigate()

popBackStack()

goBack()

Remplacez les champs NavController par des champs NavigationState.

Champ ou méthode NavController

Équivalent NavigationState

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

Obtenez l'itinéraire de niveau supérieur : parcourez la hiérarchie à partir de l'entrée actuelle de la pile de retour pour le trouver.

topLevelRoute

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 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 à l'intérieur du 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 en 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 :

  • NavigationState comporte un ensemble de routes de premier niveau (routes parentes) et une pile pour chacune d'elles. Il suit la route de premier niveau actuelle et sa pile associée.
  • Lorsqu'il accède à un nouvel itinéraire, Navigator vérifie s'il s'agit d'un itinéraire de premier niveau. 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 dans le même champ d'application que NavigationState.

val entryProvider = entryProvider {

}

Étape 5.2 : Déplacez les destinations dans 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 "Retour" imbriquée.
  • composable<T> : déplacez-le dans entryProvider et renommez-le entry en conservant le paramètre de type. Par exemple, composable<RouteA> devient entry<RouteA>.
  • dialog<T> : effectue la même opération que composable, mais ajoute des métadonnées à l'entrée comme suit : entry<T>(metadata = DialogSceneStrategy.dialog()).
  • bottomSheet : suivez la recette de la feuille inférieure ici. Ces instructions sont semblables à celles de dialog, sauf que BottomSheetSceneStrategy ne fait pas partie de la bibliothèque Navigation 3 de base. Vous devez donc le copier dans votre projet.

Agent d'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 à celui-ci par RouteA. Ce remplacement doit généralement être effectué pour la liste fournie à une barre de navigation, un rail ou un tiroir.

Vous pouvez refactoriser les fonctions d'extension NavGraphBuilder en fonctions d'extension EntryProviderScope<T>, puis les déplacer.

Obtenez les 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

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

Remplacement de NavHost par NavDisplay.

  • Supprimez NavHost et remplacez-le par NavDisplay.
  • Spécifiez entries = navigationState.toEntries(entryProvider) comme paramètre. Cela convertit l'état de navigation en entrées que NavDisplay affiche à l'aide de entryProvider.
  • Connectez NavDisplay.onBack à navigator.goBack(). Cela entraîne la mise à jour de l'état de navigation par navigator lorsque le gestionnaire de retour intégré de NavDisplay est terminé.
  • Si vous avez des destinations de dialogue, ajoutez DialogSceneStrategy au paramètre sceneStrategy de NavDisplay.

Exemple :

import androidx.navigation3.ui.NavDisplay

NavDisplay(
    entries = navigationState.toEntries(entryProvider),
    onBack = { navigator.goBack() },
    sceneStrategy = remember { DialogSceneStrategy() }
)

Étape 7 : Supprimez les dépendances Navigation 2

Supprimez toutes les importations et dépendances de bibliothèque Navigation 2.

Résumé

Félicitations ! Votre projet est désormais migré vers Navigation 3. Si vous ou votre agent d'IA rencontrez des problèmes lors de l'utilisation de ce guide, signalez un bug ici.