Компонент Navigation обеспечивает поддержку приложений Jetpack Compose. Вы можете перемещаться между компонуемыми объектами, используя преимущества инфраструктуры и функций компонента Navigation.
Для получения информации о последней альфа-версии библиотеки навигации, созданной специально для Compose, см. документацию Navigation 3 .
Настраивать
Для поддержки Compose используйте следующую зависимость в файле build.gradle
вашего модуля приложения:
классный
dependencies { def nav_version = "2.9.0" implementation "androidx.navigation:navigation-compose:$nav_version" }
Котлин
dependencies { val nav_version = "2.9.0" implementation("androidx.navigation:navigation-compose:$nav_version") }
Начать
При реализации навигации в приложении реализуйте хост навигации, график и контроллер. Для получения дополнительной информации см. Обзор навигации .
Создать NavController
Информацию о том, как создать NavController
в Compose, см. в разделе Compose статьи Создание навигационного контроллера .
Создать NavHost
Информацию о том, как создать NavHost
в Compose, см. в разделе «Составление» статьи «Разработка навигационного графика» .
Перейдите к компонуемому
Информацию о переходе к Composable см. в разделе Переход к месту назначения в документации по архитектуре.
Навигация с аргументами
Информацию о передаче аргументов между компонуемыми пунктами назначения см. в разделе «Составление» статьи «Разработка навигационного графика» .
Извлекайте сложные данные при навигации
Настоятельно рекомендуется не передавать сложные объекты данных при навигации, а вместо этого передавать минимально необходимую информацию, такую как уникальный идентификатор или другую форму идентификатора, в качестве аргументов при выполнении навигационных действий:
// Pass only the user ID when navigating to a new destination as argument
navController.navigate(Profile(id = "user1234"))
Сложные объекты должны храниться как данные в едином источнике истины, например, в слое данных. Как только вы приземлитесь в пункте назначения после навигации, вы можете загрузить требуемую информацию из единого источника истины, используя переданный идентификатор. Чтобы получить аргументы в вашей ViewModel
, которые отвечают за доступ к слою данных, используйте SavedStateHandle
ViewModel
:
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val profile = savedStateHandle.toRoute<Profile>()
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(profile.id)
// …
}
Такой подход помогает предотвратить потерю данных при изменении конфигурации и любые несоответствия при обновлении или изменении соответствующего объекта.
Более подробное объяснение того, почему следует избегать передачи сложных данных в качестве аргументов, а также список поддерживаемых типов аргументов см. в разделе Передача данных между пунктами назначения .
Глубокие ссылки
Navigation Compose поддерживает глубокие ссылки, которые также могут быть определены как часть функции composable()
. Его параметр deepLinks
принимает список объектов NavDeepLink
, которые можно быстро создать с помощью метода navDeepLink()
:
@Serializable data class Profile(val id: String)
val uri = "https://www.example.com"
composable<Profile>(
deepLinks = listOf(
navDeepLink<Profile>(basePath = "$uri/profile")
)
) { backStackEntry ->
ProfileScreen(id = backStackEntry.toRoute<Profile>().id)
}
Эти глубокие ссылки позволяют вам связать определенный URL, действие или тип MIME с компонуемым. По умолчанию эти глубокие ссылки не отображаются для внешних приложений. Чтобы сделать эти глубокие ссылки доступными извне, вы должны добавить соответствующие элементы <intent-filter>
в файл manifest.xml
вашего приложения. Чтобы включить глубокую ссылку в предыдущем примере, вы должны добавить следующее внутри элемента <activity>
манифеста:
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
Навигация автоматически переходит по глубоким ссылкам в этот компонуемый объект, когда глубокая ссылка активируется другим приложением.
Эти же глубокие ссылки можно использовать для создания PendingIntent
с соответствующей глубокой ссылкой из компонуемого объекта:
val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://www.example.com/profile/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
Затем вы можете использовать этот deepLinkPendingIntent
, как и любой другой PendingIntent
, чтобы открыть свое приложение в месте назначения глубокой ссылки.
Вложенная навигация
Информацию о создании вложенных навигационных графиков см. в разделе Вложенные графики .
Интеграция с нижней навигационной панелью
Определяя NavController
на более высоком уровне в вашей компонуемой иерархии, вы можете связать Navigation с другими компонентами, такими как нижний компонент навигации. Это позволяет вам осуществлять навигацию, выбирая значки в нижней панели.
Чтобы использовать компоненты BottomNavigation
и BottomNavigationItem
, добавьте зависимость androidx.compose.material
в ваше приложение Android.
классный
dependencies { implementation "androidx.compose.material:material:1.8.2" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Котлин
dependencies { implementation("androidx.compose.material:material:1.8.2") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Чтобы связать элементы нижней навигационной панели с маршрутами в навигационном графике, рекомендуется определить класс, например TopLevelRoute
показанный здесь, который имеет класс маршрута и значок.
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
Затем поместите эти маршруты в список, который может использоваться BottomNavigationItem
:
val topLevelRoutes = listOf(
TopLevelRoute("Profile", Profile, Icons.Profile),
TopLevelRoute("Friends", Friends, Icons.Friends)
)
В вашем BottomNavigation
composable получите текущий NavBackStackEntry
с помощью функции currentBackStackEntryAsState()
. Эта запись дает вам доступ к текущему NavDestination
. Выбранное состояние каждого BottomNavigationItem
затем можно определить, сравнив маршрут элемента с маршрутом текущего пункта назначения и его родительских пунктов назначения для обработки случаев, когда вы используете вложенную навигацию с использованием иерархии NavDestination
.
Маршрут элемента также используется для подключения лямбды onClick
к вызову navigate
, чтобы нажатие на элемент осуществляло переход к этому элементу. Используя флаги saveState
и restoreState
, состояние и обратный стек этого элемента правильно сохраняются и восстанавливаются при переключении между нижними элементами навигации.
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
topLevelRoutes.forEach { topLevelRoute ->
BottomNavigationItem(
icon = { Icon(topLevelRoute.icon, contentDescription = topLevelRoute.name) },
label = { Text(topLevelRoute.name) },
selected = currentDestination?.hierarchy?.any { it.hasRoute(topLevelRoute.route::class) } == true,
onClick = {
navController.navigate(topLevelRoute.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(navController, startDestination = Profile, Modifier.padding(innerPadding)) {
composable<Profile> { ProfileScreen(...) }
composable<Friends> { FriendsScreen(...) }
}
}
Здесь вы используете метод NavController.currentBackStackEntryAsState()
, чтобы поднять состояние navController
из функции NavHost
и поделиться им с компонентом BottomNavigation
. Это означает, что BottomNavigation
автоматически имеет самое актуальное состояние.
Взаимодействие
Если вы хотите использовать компонент «Навигация» с Compose, у вас есть два варианта:
- Определите навигационный граф с компонентом «Навигация» для фрагментов.
- Определите навигационный граф с
NavHost
в Compose с помощью Compose destinations. Это возможно только в том случае, если все экраны в навигационном графе являются компонуемыми.
Поэтому для смешанных приложений Compose и Views рекомендуется использовать компонент навигации на основе фрагментов. Фрагменты затем будут содержать экраны на основе View, экраны Compose и экраны, которые используют как Views, так и Compose. После того, как содержимое каждого фрагмента будет в Compose, следующим шагом будет связать все эти экраны вместе с Navigation Compose и удалить все фрагменты.
Переход из Compose с навигацией для фрагментов
Чтобы изменить пункты назначения внутри кода Compose, вы предоставляете события, которые могут передаваться и запускаться любым компонуемым объектом в иерархии:
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
В вашем фрагменте вы создаете мост между Compose и компонентом Navigation на основе фрагмента, находя NavController
и выполняя навигацию к месту назначения:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
В качестве альтернативы вы можете передать NavController
вниз по иерархии Compose. Однако, предоставление простых функций гораздо более пригодно для повторного использования и тестирования.
Тестирование
Отделите навигационный код от ваших компонуемых пунктов назначения, чтобы иметь возможность тестировать каждый компонуемый элемент изолированно, отдельно от компонуемого элемента NavHost
.
Это означает, что вам не следует передавать navController
напрямую в любой компонуемый объект , а вместо этого передавать обратные вызовы навигации в качестве параметров. Это позволяет всем вашим компонуемым объектам быть индивидуально тестируемыми, поскольку им не требуется экземпляр navController
в тестах.
Уровень косвенности, предоставляемый composable
лямбдой, позволяет вам отделить код навигации от самого компонуемого. Это работает в двух направлениях:
- Передавайте в ваш компонуемый объект только проанализированные аргументы.
- Передайте лямбда-выражения, которые должны запускаться компонуемым объектом для навигации, а не самим
NavController
.
Например, составной элемент ProfileScreen
, который принимает в качестве входных данных идентификатор userId
и позволяет пользователям переходить на страницу профиля друга, может иметь следующую сигнатуру:
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
Таким образом, ProfileScreen
composable работает независимо от Navigation, что позволяет тестировать его независимо. composable
лямбда инкапсулирует минимальную логику, необходимую для преодоления разрыва между Navigation API и вашим композицией:
@Serializable data class Profile(id: String)
composable<Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(userId = profile.id) { friendUserId ->
navController.navigate(route = Profile(id = friendUserId))
}
}
Рекомендуется писать тесты, охватывающие требования к навигации вашего приложения, путем тестирования NavHost
, действий навигации, передаваемых вашим компонуемым элементам, а также ваших отдельных компонуемых экранов.
Тестирование NavHost
Чтобы начать тестирование NavHost
, добавьте следующую зависимость тестирования навигации:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
Оберните NavHost
вашего приложения в компонуемый объект, который принимает NavHostController
в качестве параметра.
@Composable
fun AppNavHost(navController: NavHostController){
NavHost(navController = navController){ ... }
}
Теперь вы можете протестировать AppNavHost
и всю навигационную логику, определенную внутри NavHost
, передав экземпляр артефакта тестирования навигации TestNavHostController
. Тест пользовательского интерфейса, который проверяет начальный пункт назначения вашего приложения и NavHost
, будет выглядеть следующим образом:
class NavigationTest {
@get:Rule
val composeTestRule = createComposeRule()
lateinit var navController: TestNavHostController
@Before
fun setupAppNavHost() {
composeTestRule.setContent {
navController = TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(ComposeNavigator())
AppNavHost(navController = navController)
}
}
// Unit test
@Test
fun appNavHost_verifyStartDestination() {
composeTestRule
.onNodeWithContentDescription("Start Screen")
.assertIsDisplayed()
}
}
Тестирование навигационных действий
Вы можете протестировать реализацию навигации несколькими способами: щелкнув мышью по элементам пользовательского интерфейса, а затем либо проверив отображаемый пункт назначения, либо сравнив ожидаемый маршрут с текущим маршрутом.
Поскольку вы хотите протестировать реализацию вашего конкретного приложения, щелчки по пользовательскому интерфейсу предпочтительнее. Чтобы узнать, как тестировать это вместе с отдельными составными функциями в изоляции, обязательно ознакомьтесь с лабораторией по тестированию в Jetpack Compose .
Вы также можете использовать navController
для проверки своих утверждений, сравнивая текущий маршрут с ожидаемым, используя currentBackStackEntry
navController
:
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
Для получения дополнительных рекомендаций по основам тестирования Compose см. Тестирование макета Compose и Тестирование в Jetpack Compose codelab. Чтобы узнать больше о расширенном тестировании кода навигации, посетите руководство Test Navigation .
Узнать больше
Чтобы узнать больше о Jetpack Navigation, ознакомьтесь с разделом Начало работы с компонентом Navigation или пройдите практическую работу по Jetpack Compose Navigation .
Чтобы узнать, как разработать навигацию вашего приложения, чтобы она адаптировалась к различным размерам экрана, ориентациям и форм-факторам, см . раздел Навигация для адаптивных пользовательских интерфейсов .
Чтобы узнать о более продвинутой реализации навигации Compose в модульном приложении, включая такие концепции, как вложенные графики и интеграция нижней панели навигации, ознакомьтесь с приложением Now in Android на GitHub.
Образцы
Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Material Design 2 в Compose
- Перенести навигацию Jetpack в навигацию Compose
- Где поднять состояние