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

Для переноса вашего приложения с Navigation 2 на Navigation 3 выполните следующие шаги:

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

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

В этом задании мы воспользуемся данным руководством для перехода к навигации 3.

Migrate from Navigation 2 to Navigation 3 using the official
migration guide.

Использование подсказок ИИ

Подсказки ИИ предназначены для использования в Gemini в Android Studio.

Подробнее о Gemini в Studio можно узнать здесь: https://developer.android.com/studio/gemini/overview

Поделитесь своим мнением

Если у вас возникнут проблемы, сообщите о них здесь .

Подготовка

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

Предварительные требования

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

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

Данное руководство исходит из следующих предположений относительно вас и вашего проекта:

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

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

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

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

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

Функции, поддерживаемые с помощью рецептов.

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

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

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

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

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

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

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

Шаг 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 , сохранив параметр type. Например, composable<RouteA> становится entry<RouteA> .
  • dialog<T> : Сделайте то же самое, что и composable , но добавьте метаданные к записи следующим образом: entry<T>(metadata = DialogSceneStrategy.dialog()) .
  • bottomSheet : Следуйте инструкциям по созданию нижнего листа, приведенным здесь . Это похоже на инструкции для dialog , за исключением того, что BottomSheetSceneStrategy не является частью основной библиотеки Navigation 3, поэтому вам следует скопировать его в свой проект.

AI-агент : При удалении маршрутов, используемых для идентификации вложенного графа, замените все ссылки на удаленный маршрут типом, используемым для идентификации первого дочернего элемента во вложенном графе. Например, если исходный код выглядит так: 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. Если у вас или вашего ИИ-агента возникли какие-либо проблемы при использовании этого руководства, сообщите об ошибке здесь .