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

Компонент 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 в 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, вы предоставляете события, которые могут передаваться и запускаться любым компонуемым объектом в иерархии:

@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.

Образцы

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