Компонент навигации обеспечивает поддержку приложений Jetpack Compose. Вы можете перемещаться между составными объектами, используя преимущества инфраструктуры и функций компонента навигации.
Настраивать
Для поддержки Compose используйте следующую зависимость в файле build.gradle
вашего модуля приложения:
классный
dependencies { def nav_version = "2.7.7" implementation "androidx.navigation:navigation-compose:$nav_version" }
Котлин
dependencies { val nav_version = "2.7.7" implementation("androidx.navigation:navigation-compose:$nav_version") }
Начать
При реализации навигации в приложении реализуйте хост навигации, график и контроллер. Дополнительную информацию см. в разделе Обзор навигации .
Создайте NavController
Информацию о том, как создать NavController
в Compose, см. в разделе «Создание» статьи «Создание контроллера навигации» .
Создать NavHost
Информацию о том, как создать NavHost
в Compose, см. в разделе «Создание» статьи «Создание навигационного графа» .
Перейдите к составному элементу
Информацию о переходе к Composable см. в разделе «Навигация к месту назначения» документации по архитектуре.
Навигация с аргументами
Navigation Compose также поддерживает передачу аргументов между составными пунктами назначения. Для этого вам нужно добавить заполнители аргументов в свой маршрут, аналогично тому, как вы добавляете аргументы в глубокую ссылку при использовании базовой библиотеки навигации:
NavHost(startDestination = "profile/{userId}") {
...
composable("profile/{userId}") {...}
}
По умолчанию все аргументы анализируются как строки. Параметр arguments
функции composable()
принимает список объектов NamedNavArgument
. Вы можете быстро создать NamedNavArgument
с помощью метода navArgument()
, а затем указать его точный type
:
NavHost(startDestination = "profile/{userId}") {
...
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
}
Вам следует извлечь аргументы из NavBackStackEntry
, который доступен в лямбда-выражении функции composable()
.
composable("profile/{userId}") { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
Чтобы передать аргумент в пункт назначения, вам нужно добавить его в маршрут при выполнении вызова navigate
:
navController.navigate("profile/user1234")
Список поддерживаемых типов см. в разделе Передача данных между пунктами назначения .
Получение сложных данных при навигации
Настоятельно рекомендуется не передавать сложные объекты данных при навигации, а вместо этого передавать минимально необходимую информацию, например уникальный идентификатор или другую форму идентификатора, в качестве аргументов при выполнении действий навигации:
// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")
Сложные объекты должны храниться как данные в одном источнике достоверности, например на уровне данных. Как только вы приземлитесь в пункте назначения после навигации, вы сможете загрузить необходимую информацию из единого источника истины, используя переданный идентификатор. Чтобы получить аргументы в вашей ViewModel
, отвечающие за доступ к уровню данных, используйте SavedStateHandle
ViewModel
:
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val userId: String = checkNotNull(savedStateHandle["userId"])
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)
// …
}
Этот подход помогает предотвратить потерю данных во время изменений конфигурации и любые несоответствия при обновлении или мутации рассматриваемого объекта.
Более подробное объяснение того, почему следует избегать передачи сложных данных в качестве аргументов, а также список поддерживаемых типов аргументов см. в разделе Передача данных между пунктами назначения .
Добавьте необязательные аргументы
Navigation Compose также поддерживает дополнительные аргументы навигации. Необязательные аргументы отличаются от обязательных двумя способами:
- Они должны быть включены с использованием синтаксиса параметров запроса (
"?argName={argName}"
). - Они должны иметь установленное
defaultValue
или иметьnullable = true
(что неявно устанавливает значение по умолчанию равноеnull
).
Это означает, что все необязательные аргументы должны быть явно добавлены в функцию composable()
в виде списка:
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
Теперь, даже если в пункт назначения не передан аргумент, вместо него используется defaultValue
«user1234».
Структура обработки аргументов через маршруты означает, что ваши компонуемые объекты остаются полностью независимыми от навигации и делают их гораздо более тестируемыми.
Глубокие ссылки
Navigation Compose поддерживает неявные глубокие ссылки, которые также можно определить как часть функции composable()
. Его параметр deepLinks
принимает список объектов NavDeepLink
, которые можно быстро создать с помощью метода navDeepLink()
:
val uri = "https://www.example.com"
composable(
"profile?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("id"))
}
Эти глубокие ссылки позволяют связать определенный URL-адрес, действие или тип MIME с составным объектом. По умолчанию эти глубокие ссылки не доступны внешним приложениям. Чтобы сделать эти глубокие ссылки доступными извне, необходимо добавить соответствующие элементы <intent-filter>
в файл manifest.xml
вашего приложения.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/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
Затем вы можете использовать этот deepLinkPendingIntent
как и любой другой PendingIntent
чтобы открыть свое приложение в месте назначения глубокой ссылки.
Вложенная навигация
Информацию о том, как создавать вложенные графики навигации, см. в разделе Вложенные графики .
Интеграция с нижней панелью навигации
Определив NavController
на более высоком уровне в составной иерархии, вы можете соединить навигацию с другими компонентами, такими как нижний компонент навигации. Это позволит вам перемещаться, выбирая значки на нижней панели.
Чтобы использовать компоненты BottomNavigation
и BottomNavigationItem
, добавьте зависимость androidx.compose.material
в свое приложение Android.
классный
dependencies { implementation "androidx.compose.material:material:1.6.8" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Котлин
dependencies { implementation("androidx.compose.material:material:1.6.8") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Чтобы связать элементы нижней панели навигации с маршрутами в графе навигации, рекомендуется определить запечатанный класс, например Screen
, показанный здесь, который содержит маршрут и идентификатор ресурса String для пунктов назначения.
sealed class Screen(val route: String, @StringRes val resourceId: Int) {
object Profile : Screen("profile", R.string.profile)
object FriendsList : Screen("friendslist", R.string.friends_list)
}
Затем поместите эти элементы в список, который может использоваться BottomNavigationItem
:
val items = listOf(
Screen.Profile,
Screen.FriendsList,
)
В компонуемом элементе BottomNavigation
получите текущий NavBackStackEntry
с помощью функции currentBackStackEntryAsState()
. Эта запись дает вам доступ к текущему NavDestination
. Затем выбранное состояние каждого BottomNavigationItem
можно определить путем сравнения маршрута элемента с маршрутом текущего пункта назначения и его родительских пунктов назначения для обработки случаев, когда вы используете вложенную навигацию с использованием иерархии NavDestination
.
Маршрут элемента также используется для подключения лямбды onClick
к вызову navigate
, чтобы нажатие на элемент переходило к этому элементу. При использовании флагов saveState
и restoreState
состояние и задний стек этого элемента правильно сохраняются и восстанавливаются при переключении между элементами нижней навигации.
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.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 = Screen.Profile.route, Modifier.padding(innerPadding)) {
composable(Screen.Profile.route) { Profile(navController) }
composable(Screen.FriendsList.route) { FriendsList(navController) }
}
}
Здесь вы воспользуетесь методом NavController.currentBackStackEntryAsState()
чтобы поднять состояние navController
из функции NavHost
и поделиться им с компонентом BottomNavigation
. Это означает, что BottomNavigation
автоматически получает самое актуальное состояние.
Безопасность типов в Navigation Compose
Код на этой странице не является типобезопасным. Вы можете вызвать функцию navigate()
с несуществующими маршрутами или неправильными аргументами. Однако вы можете структурировать свой навигационный код так, чтобы он был типобезопасным во время выполнения. Поступая так, вы сможете избежать сбоев и убедиться, что:
- Аргументы, которые вы предоставляете при переходе к пункту назначения или графику навигации, имеют правильные типы и присутствуют все необходимые аргументы.
- Аргументы, которые вы получаете из
SavedStateHandle
имеют правильные типы.
Дополнительные сведения об этом см. в разделах Безопасность типов в Kotlin DSL и Navigation Compose .
Совместимость
Если вы хотите использовать компонент навигации с Compose, у вас есть два варианта:
- Определите граф навигации с помощью компонента «Навигация» для фрагментов.
- Определите граф навигации с помощью
NavHost
в Compose, используя пункты назначения Compose. Это возможно только в том случае, если все экраны навигационного графа являются составными.
Поэтому для смешанных приложений Compose и Views рекомендуется использовать компонент навигации на основе фрагментов. Затем фрагменты будут содержать экраны на основе просмотра, экраны создания и экраны, использующие как представления, так и создание. Как только содержимое каждого фрагмента окажется в Compose, следующим шагом будет объединение всех этих экранов вместе с помощью Navigation Compose и удаление всех фрагментов.
Переход от создания с помощью навигации по фрагментам
Чтобы изменить пункты назначения внутри кода Compose, вы предоставляете события, которые могут быть переданы и запущены любым компонуемым объектом в иерархии:
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
В своем фрагменте вы создаете мост между Compose и компонентом навигации на основе фрагментов, находя NavController
и переходя к месту назначения:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
Альтернативно вы можете передать NavController
вниз по иерархии Compose. Однако предоставление простых функций гораздо более пригодно для повторного использования и тестирования.
Тестирование
Отделите код навигации от составных пунктов назначения, чтобы можно было тестировать каждый составной объект изолированно, отдельно от составного объекта NavHost
.
Это означает, что вам не следует передавать navController
непосредственно в любой компонуемый объект , а вместо этого передавать обратные вызовы навигации в качестве параметров. Это позволяет индивидуально тестировать все ваши составные элементы, поскольку для них не требуется экземпляр navController
в тестах.
Уровень косвенности, обеспечиваемый composable
лямбдой, позволяет вам отделить код навигации от самого составного объекта. Это работает в двух направлениях:
- Передавайте в компонуемый объект только проанализированные аргументы.
- Передавайте лямбды, которые должны запускаться компонуемым объектом для навигации, а не самим
NavController
.
Например, составной Profile
, который принимает userId
в качестве входных данных и позволяет пользователям переходить на страницу профиля друга, может иметь подпись:
@Composable
fun Profile(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
Таким образом, составной Profile
работает независимо от навигации, что позволяет тестировать его независимо. composable
лямбда-выражение будет инкапсулировать минимальную логику, необходимую для устранения разрыва между API навигации и вашим компонуемым объектом:
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
navController.navigate("profile?userId=$friendUserId")
}
}
Рекомендуется писать тесты, которые охватывают требования к навигации вашего приложения, проверяя NavHost
, действия навигации, передаваемые вашим составным объектам, а также отдельные составные элементы экрана.
Тестирование NavHost
Чтобы начать тестирование вашего NavHost
, добавьте следующую зависимость тестирования навигации:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
Вы можете настроить свой объект тестирования NavHost
и передать ему экземпляр экземпляра navController
. Для этого артефакт тестирования навигации предоставляет 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
для проверки своих утверждений, сравнивая текущий маршрут String с ожидаемым, используя currentBackStackEntry
navController
:
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
val route = navController.currentBackStackEntry?.destination?.route
assertEquals(route, "profiles")
}
Дополнительные рекомендации по основам тестирования Compose см. в разделах Тестирование макета Compose и лаборатории тестирования Compose в Jetpack . Чтобы узнать больше о расширенном тестировании кода навигации, посетите руководство по тестированию навигации .
Узнать больше
Дополнительные сведения о навигации Jetpack см. в разделе Начало работы с компонентом навигации или воспользуйтесь лабораторной работой по написанию кода Jetpack Compose Navigation .
Чтобы узнать, как спроектировать навигацию вашего приложения так, чтобы она адаптировалась к различным размерам, ориентациям и форм-факторам экрана, см. раздел «Навигация для адаптивных пользовательских интерфейсов» .
Чтобы узнать о более продвинутой реализации навигации Compose в модульном приложении, включая такие концепции, как вложенные графики и интеграция нижней панели навигации, ознакомьтесь с приложением Now in Android на GitHub.
Образцы
Рекомендуется для вас
- Примечание. Текст ссылки отображается, когда JavaScript отключен.
- Material Design 2 в Compose
- Перенос навигации Jetpack в Navigation Compose
- Где поднять состояние