Навигация с помощью Compose

Компонент навигации обеспечивает поддержку приложений 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 в 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.

Образцы

{% дословно %} {% дословно %} {% дословно %} {% дословно %}