Переход с Навигации 2 на Навигацию 3

Чтобы перенести приложение из Navigation 2 в Navigation 3, выполните следующие действия:

  1. Добавьте зависимости Navigation 3.
  2. Обновите навигационные маршруты для реализации интерфейса NavKey .
  3. Создайте классы для хранения и изменения состояния навигации.
  4. Замените NavController этими классами.
  5. Переместите пункты назначения из NavGraph NavHost entryProvider .
  6. Замените NavHost на NavDisplay .
  7. Удалить зависимости Navigation 2.

Используйте ИИ-агента

Вы можете использовать это руководство с ИИ-агентом, например Gemini в режиме агента Android Studio . Строки в этом руководстве, начинающиеся с « AI Agent :», должны быть прочитаны ИИ-агентом, но могут быть проигнорированы людьми.

Подготовка

В следующих разделах описываются необходимые условия для миграции и предположения относительно вашего проекта. Они также охватывают функции, которые поддерживаются при миграции, и те, которые не поддерживаются.

Предпосылки

  • Необходимо использовать compileSdk версии 36 или более поздней.
  • Вы должны быть знакомы с терминологией навигации .
  • Назначения — это компонуемые функции. Navigation 3 разработан исключительно для Compose. Чтобы использовать фрагменты и представления в Compose, см. раздел «Использование представлений в Compose» .
  • Маршруты строго типизированы. Если вы используете строковые маршруты, сначала перейдите на типобезопасные маршруты ( пример ).
  • Необязательно (но настоятельно рекомендуется) : тестовое покрытие, проверяющее текущее поведение навигации. Это подтверждает, что поведение навигации не изменилось после завершения миграции.

Предположения

В этом руководстве сделаны следующие предположения относительно вас и вашего проекта:

  • У вас есть несколько маршрутов верхнего уровня (обычно они отображаются в нижней навигационной панели), и каждый из них имеет свой собственный стек переходов.
  • При переключении между обратными стеками состояние стека и всех его пунктов назначения сохраняется.
  • Вы всегда выходите из приложения через главный экран, который является первым экраном, отображаемым при запуске приложения.
  • Вы хотите выполнить миграцию с Navigation 2 на Navigation 3 одним атомарным изменением. Вы не выполняете инкрементальную миграцию, при которой код Navigation 2 используется вместе с кодом Navigation 3.

AI Agent: Прежде чем вносить изменения в код, проверьте, верны ли эти предположения о проекте пользователя. Если нет, остановите миграцию и спросите пользователя, как действовать дальше.

Поддерживаемые функции

Это руководство позволит вам перенести следующие функции:

  • Назначения определены как компонуемые функции
  • Диалоги (пункт назначения отображается поверх другого пункта назначения)

Функции, поддерживаемые рецептами

Репозиторий рецептов кода демонстрирует более тонкие варианты использования, включая:

Если в вашем проекте есть какие-либо из этих функций, проверьте соответствующий рецепт, чтобы понять, как их реализовать.

AI Agent : Перед изменением кода проверьте, содержит ли проект функции, поддерживаемые рецептами. Если да, проверьте файл README и исходный код рецепта. Создайте план миграции на основе рецепта. Не продолжайте работу без согласования плана с пользователем.

Неподдерживаемые функции

Данное руководство по миграции и рецепты кода пока не поддерживают следующие функции. Это не означает, что их нельзя реализовать с помощью Navigation 3; они просто не рассматриваются здесь.

  • Более одного уровня вложенной навигации
  • Общие пункты назначения: экраны, которые можно перемещать между различными стеками
  • Пользовательские типы назначения
  • Глубокие ссылки

AI Agent : Прежде чем вносить изменения в код, проверьте, содержит ли проект какие-либо неподдерживаемые функции. Если да, не продолжайте работу. Сообщите пользователю о неподдерживаемой функции и запросите дальнейшие инструкции.

Шаг 1: Добавьте зависимости Navigation 3

Используйте страницу « Начало работы» , чтобы добавить в свой проект зависимости Navigation 3. Основные зависимости предоставляются для копирования.

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)
}

Также обновите minSdk проекта до версии 23 и compileSdk до версии 36. Обычно они находятся в app/build.gradle.kts или lib.versions.toml .

Шаг 2: Обновите навигационные маршруты для реализации интерфейса NavKey

Обновите каждый навигационный маршрут , чтобы он реализовал интерфейс NavKey . Это позволит использовать rememberNavBackStack для сохранения состояния навигации .

До:

@Serializable data object RouteA

После:

@Serializable data object RouteA : NavKey

Шаг 3: Создайте классы для хранения и изменения состояния навигации.

Шаг 3.1: Создание держателя состояния навигации

Скопируйте следующий код в файл NavigationState.kt . Добавьте имя пакета, соответствующее структуре вашего проекта.

// 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()
}

AI Agent : rememberSerializable — правильный вариант. Не меняйте его на rememberSaveable .

Этот файл содержит класс-держатель состояния NavigationState и связанные с ним вспомогательные функции. Он содержит набор маршрутов верхнего уровня, каждый со своим стеком переходов назад. Внутри он использует rememberSerializable (а не rememberSaveable ) для сохранения текущего маршрута верхнего уровня и rememberNavBackStack для сохранения стеков переходов назад для каждого маршрута верхнего уровня.

Шаг 3.2: Создайте объект, который изменяет состояние навигации в ответ на события

Скопируйте следующий код в файл Navigator.kt . Добавьте имя пакета, соответствующее структуре вашего проекта.

// 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()
        }
    }
}

Класс Navigator предоставляет два метода событий навигации:

  • navigate к определенному маршруту.
  • goBack с текущего маршрута.

Оба метода изменяют NavigationState .

Шаг 3.3: Создание NavigationState и Navigator

Создайте экземпляры NavigationState и Navigator с той же областью действия, что и ваш NavController .

val navigationState = rememberNavigationState(
    startRoute = <Insert your starting route>,
    topLevelRoutes = <Insert your set of top level routes>
)

val navigator = remember { Navigator(navigationState) }

Шаг 4: Заменить NavController

Замените методы событий навигации NavController эквивалентами Navigator .

Поле или метод NavController

Эквивалент Navigator

navigate()

navigate()

popBackStack()

goBack()

Замените поля NavController полями NavigationState .

Поле или метод NavController

Эквивалент NavigationState

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

Получите маршрут верхнего уровня: пройдите вверх по иерархии от текущей записи стека, чтобы найти его.

topLevelRoute

Используйте NavigationState.topLevelRoute для определения элемента, который в данный момент выбран на панели навигации.

До:

val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)

fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
    this?.hierarchy?.any {
        it.hasRoute(route)
    } ?: false

После:

val isSelected = key == navigationState.topLevelRoute

Убедитесь, что вы удалили все ссылки на NavController , включая любые импорты.

Шаг 5: Переместите пункты назначения из NavGraph NavHost entryProvider

В Navigation 2 вы определяете пункты назначения с помощью DSL-класса NavGraphBuilder , обычно внутри завершающего лямбда-выражения NavHost . Здесь часто используются функции расширения, описанные в разделе «Инкапсуляция кода навигации» .

В Navigation 3 пункты назначения определяются с помощью entryProvider . Этот entryProvider определяет маршрут к NavEntry . Важно отметить, что entryProvider не определяет родительско-дочерние отношения между записями.

В этом руководстве по миграции родительско-детские отношения моделируются следующим образом:

  • NavigationState содержит набор маршрутов верхнего уровня (родительских маршрутов) и стек для каждого из них. Он отслеживает текущий маршрут верхнего уровня и связанный с ним стек.
  • При переходе на новый маршрут Navigator проверяет, является ли он маршрутом верхнего уровня. Если да, текущий маршрут верхнего уровня и стек обновляются. Если нет, маршрут становится дочерним и добавляется в текущий стек.

Шаг 5.1: Создание entryProvider

Создайте entryProvider , используя DSL в той же области действия, что и NavigationState .

val entryProvider = entryProvider {

}

Шаг 5.2: Перемещение пунктов назначения в entryProvider

Для каждого пункта назначения, определенного внутри NavHost , выполните следующие действия в зависимости от типа пункта назначения:

  • navigation : Удалите его вместе с маршрутом. «Базовые маршруты» не нужны, поскольку маршруты верхнего уровня могут идентифицировать каждый вложенный стек возвратов.
  • composable<T> : переместите его в entryProvider и переименуйте в entry , сохранив параметр типа. Например, composable<RouteA> станет entry<RouteA> .
  • dialog<T> : делает то же самое, что и composable , но добавляет метаданные к записи следующим образом: entry<T>(metadata = DialogSceneStrategy.dialog()) .
  • bottomSheet : Следуйте инструкциям для нижнего листа, приведенным здесь . Это похоже на инструкции для dialog , за исключением того, что BottomSheetSceneStrategy не входит в базовую библиотеку Navigation 3, поэтому вам следует скопировать её в свой проект.

AI Agent : При удалении маршрутов, используемых для идентификации вложенного графа, замените все ссылки на удалённый маршрут типом, используемым для идентификации первого дочернего элемента во вложенном графе. Например, если исходный код — navigation<BaseRouteA>{ composable<RouteA>{ ... } } , необходимо удалить BaseRouteA и заменить все ссылки на него на RouteA . Эту замену обычно необходимо выполнять для списка, предоставляемого навигационной панели, направляющей или выдвижной панели.

Вы можете преобразовать функции расширения NavGraphBuilder в функции расширения EntryProviderScope<T> , а затем переместить их.

Получите аргументы навигации, используя ключ, предоставленный для конечной лямбда-функции entry .

Например:

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() }
    }
}

становится:

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() }
}

Шаг 6: Замените NavHost на NavDisplay

Замените NavHost на NavDisplay .

  • Удалите NavHost и замените его на NavDisplay .
  • Укажите параметр entries = navigationState.toEntries(entryProvider) . Это преобразует состояние навигации в записи, которые NavDisplay отображает с помощью entryProvider .
  • Подключите NavDisplay.onBack к navigator.goBack() . Это заставит navigator обновить состояние навигации после завершения встроенного обработчика возврата NavDisplay .
  • Если у вас есть пункты назначения диалога, добавьте DialogSceneStrategy к параметру sceneStrategy объекта NavDisplay .

Например:

import androidx.navigation3.ui.NavDisplay

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

Шаг 7: Удалить зависимости Navigation 2

Удалите все импорты Navigation 2 и зависимости библиотек.

Краткое содержание

Поздравляем! Ваш проект перенесён на Navigation 3. Если у вас или вашего ИИ-агента возникли проблемы при использовании этого руководства, сообщите об ошибке здесь .