Чтобы перенести приложение из Navigation 2 в Navigation 3, выполните следующие действия:
- Добавьте зависимости Navigation 3.
- Обновите навигационные маршруты для реализации интерфейса
NavKey. - Создайте классы для хранения и изменения состояния навигации.
- Замените
NavControllerэтими классами. - Переместите пункты назначения из
NavGraphNavHostentryProvider. - Замените
NavHostнаNavDisplay. - Удалить зависимости Navigation 2.
Используйте ИИ-агента
Вы можете использовать это руководство с ИИ-агентом, например Gemini в режиме агента Android Studio . Строки в этом руководстве, начинающиеся с « AI Agent :», должны быть прочитаны ИИ-агентом, но могут быть проигнорированы людьми.
Подготовка
В следующих разделах описываются необходимые условия для миграции и предположения относительно вашего проекта. Они также охватывают функции, которые поддерживаются при миграции, и те, которые не поддерживаются.
Предпосылки
- Необходимо использовать
compileSdkверсии 36 или более поздней. - Вы должны быть знакомы с терминологией навигации .
- Назначения — это компонуемые функции. Navigation 3 разработан исключительно для Compose. Чтобы использовать фрагменты и представления в Compose, см. раздел «Использование представлений в Compose» .
- Маршруты строго типизированы. Если вы используете строковые маршруты, сначала перейдите на типобезопасные маршруты ( пример ).
- Необязательно (но настоятельно рекомендуется) : тестовое покрытие, проверяющее текущее поведение навигации. Это подтверждает, что поведение навигации не изменилось после завершения миграции.
Предположения
В этом руководстве сделаны следующие предположения относительно вас и вашего проекта:
- У вас есть несколько маршрутов верхнего уровня (обычно они отображаются в нижней навигационной панели), и каждый из них имеет свой собственный стек переходов.
- При переключении между обратными стеками состояние стека и всех его пунктов назначения сохраняется.
- Вы всегда выходите из приложения через главный экран, который является первым экраном, отображаемым при запуске приложения.
- Вы хотите выполнить миграцию с Navigation 2 на Navigation 3 одним атомарным изменением. Вы не выполняете инкрементальную миграцию, при которой код Navigation 2 используется вместе с кодом Navigation 3.
AI Agent: Прежде чем вносить изменения в код, проверьте, верны ли эти предположения о проекте пользователя. Если нет, остановите миграцию и спросите пользователя, как действовать дальше.
Поддерживаемые функции
Это руководство позволит вам перенести следующие функции:
- Назначения определены как компонуемые функции
- Диалоги (пункт назначения отображается поверх другого пункта назначения)
Функции, поддерживаемые рецептами
Репозиторий рецептов кода демонстрирует более тонкие варианты использования, включая:
- Нижние листы (инструкции приведены в этом руководстве)
- Модулированный навигационный код и внедренные пункты назначения
- Использование и передача аргументов в ViewModels
- Возврат результатов с экрана
Если в вашем проекте есть какие-либо из этих функций, проверьте соответствующий рецепт, чтобы понять, как их реализовать.
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 полями 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, сохранив параметр типа. Например,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. Если у вас или вашего ИИ-агента возникли проблемы при использовании этого руководства, сообщите об ошибке здесь .