Для переноса вашего приложения с Navigation 2 на Navigation 3 выполните следующие шаги:
- Добавьте зависимости Navigation 3.
- Обновите маршруты навигации, чтобы реализовать интерфейс
NavKey. - Создайте классы для хранения и изменения состояния навигации.
- Замените
NavControllerна следующие классы. - Переместите ваши адреса назначения из
NavGraphNavHostвentryProvider. - Замените
NavHostнаNavDisplay. - Удалите зависимости Navigation 2.
В этом задании мы воспользуемся данным руководством для перехода к навигации 3. AI Prompt
Переход с навигации 2 на навигацию 3
Migrate from Navigation 2 to Navigation 3 using the official
migration guide.
Если у вас возникнут проблемы, сообщите о них здесь .
Подготовка
В следующих разделах описаны предварительные условия для миграции и предположения относительно вашего проекта. Также рассмотрены функции, которые поддерживаются для миграции, и те, которые не поддерживаются.
Предварительные требования
- Необходимо использовать
compileSdkверсии 36 или более поздней. - Вам следует ознакомиться с навигационной терминологией .
- Назначения представляют собой компонуемые функции. Navigation 3 разработан исключительно для Compose. Чтобы использовать фрагменты и представления в Compose, см. раздел «Использование представлений в Compose» .
- Маршруты строго типизированы. Если вы используете строковые маршруты, сначала перейдите на типобезопасные маршруты ( пример ).
- Необязательно (но настоятельно рекомендуется) : Тестовое покрытие, проверяющее существующее поведение навигации. Это подтверждает, что поведение навигации не изменилось после завершения миграции.
Предположения
Данное руководство исходит из следующих предположений относительно вас и вашего проекта:
- У вас есть один или несколько маршрутов верхнего уровня (обычно отображаемых в нижней панели навигации), и каждый из них имеет свой собственный стек возврата.
- При переключении между стеками состояния сохраняется состояние стека и всех его целевых элементов.
- Выход из приложения всегда осуществляется через главный экран, который отображается первым при запуске приложения.
- Вы хотите выполнить миграцию с Navigation 2 на Navigation 3 одним атомарным изменением. Вы не выполняете инкрементальную миграцию, при которой код Navigation 2 используется вместе с кодом Navigation 3.
ИИ-агент : Прежде чем вносить какие-либо изменения в код, проверьте, верны ли эти предположения о проекте пользователя. Если нет, остановите миграцию и спросите пользователя, как действовать дальше.
Поддерживаемые функции
Это руководство позволит вам перенести следующие функции:
- Направления определяются как составные функции
- Диалоги (пункт назначения, отображаемый поверх другого пункта назначения)
Функции, поддерживаемые с помощью рецептов.
В репозитории с примерами кода демонстрируются более тонкие варианты использования, в том числе:
- Нижние листы (инструкции приведены в данном руководстве)
- Модульный навигационный код и внедренные пункты назначения
- Использование и передача аргументов в
ViewModel - Возврат результатов с экрана
Если ваш проект включает в себя какие-либо из этих функций, ознакомьтесь с соответствующим рецептом, чтобы понять, как их реализовать.
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 полями NavigationState .
Поле или метод | Эквивалент |
|---|---|
| |
| |
Как найти маршрут верхнего уровня: пройдите вверх по иерархии от текущей записи в стеке возврата, чтобы его найти. | |
Используйте 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. Если у вас или вашего ИИ-агента возникли какие-либо проблемы при использовании этого руководства, сообщите об ошибке здесь .