Komponent nawigacji obsługuje aplikacje Jetpack Compose. Możesz przechodzić między funkcjami kompozycyjnymi, korzystając z infrastruktury i funkcji komponentu Navigation.
Najnowszą bibliotekę nawigacji w wersji alfa, stworzoną specjalnie z myślą o Compose, znajdziesz w dokumentacji Navigation 3.
Konfiguracja
Aby obsługiwać Compose, użyj w pliku build.gradle
modułu aplikacji tej zależności:
Groovy
dependencies { def nav_version = "2.9.1" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.9.1" implementation("androidx.navigation:navigation-compose:$nav_version") }
Rozpocznij
Podczas wdrażania nawigacji w aplikacji zaimplementuj hosta nawigacji, wykres i kontroler. Więcej informacji znajdziesz w omówieniu Nawigacji.
Tworzenie NavController
Informacje o tworzeniu NavController
w Compose znajdziesz w sekcji Compose w artykule Tworzenie kontrolera nawigacji.
Tworzenie elementu NavHost
Informacje o tworzeniu elementu NavHost
w Compose znajdziesz w sekcji Compose w artykule Projektowanie wykresu nawigacji.
Przechodzenie do funkcji kompozycyjnej
Informacje o przechodzeniu do funkcji Composable znajdziesz w sekcji Przechodzenie do miejsca docelowego w dokumentacji architektury.
Nawigowanie za pomocą argumentów
Informacje o przekazywaniu argumentów między miejscami docelowymi w kompozycji znajdziesz w sekcji poświęconej kompozycji w artykule Projektowanie wykresu nawigacji.
Pobieranie złożonych danych podczas nawigacji
Podczas nawigacji zdecydowanie nie zaleca się przekazywania złożonych obiektów danych. Zamiast tego podczas wykonywania działań nawigacyjnych należy przekazywać jako argumenty minimum niezbędnych informacji, takich jak unikalny identyfikator lub inna forma identyfikatora:
// Pass only the user ID when navigating to a new destination as argument
navController.navigate(Profile(id = "user1234"))
Złożone obiekty powinny być przechowywane jako dane w jednym źródle informacji, np. w warstwie danych. Po dotarciu do miejsca docelowego możesz za pomocą przekazanego identyfikatora wczytać wymagane informacje z jednego źródła danych. Aby pobrać argumenty w funkcji ViewModel
, która odpowiada za dostęp do warstwy danych, użyj funkcji SavedStateHandle
funkcji 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)
// …
}
Takie podejście pomaga zapobiegać utracie danych podczas wprowadzania zmian w konfiguracji i wszelkim niespójnościom podczas aktualizowania lub modyfikowania danego obiektu.
Bardziej szczegółowe wyjaśnienie, dlaczego należy unikać przekazywania złożonych danych jako argumentów, oraz listę obsługiwanych typów argumentów znajdziesz w artykule Przekazywanie danych między miejscami docelowymi.
Precyzyjne linki
Navigation Compose obsługuje precyzyjne linki, które można zdefiniować w ramach funkcji composable()
. Jego parametr deepLinks
akceptuje listę obiektów NavDeepLink
, które można szybko utworzyć za pomocą metody 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)
}
Te precyzyjne linki umożliwiają powiązanie określonego adresu URL, działania lub typu MIME z komponentem. Domyślnie te precyzyjne linki nie są udostępniane aplikacjom zewnętrznym. Aby udostępnić te precyzyjne linki na zewnątrz, musisz dodać odpowiednie elementy <intent-filter>
do pliku manifest.xml
aplikacji. Aby włączyć precyzyjny link w przykładzie powyżej, dodaj w elemencie <activity>
pliku manifestu te elementy:
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
Gdy precyzyjny link zostanie wywołany przez inną aplikację, nawigacja automatycznie przekieruje do tego komponentu.
Tych samych precyzyjnych linków można też używać do tworzenia PendingIntent
z odpowiednim precyzyjnym linkiem z elementu kompozycyjnego:
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)
}
Możesz użyć tego deepLinkPendingIntent
tak jak każdego innego PendingIntent
, aby otworzyć aplikację w miejscu docelowym precyzyjnego linku.
Zagnieżdżona nawigacja
Informacje o tworzeniu zagnieżdżonych wykresów nawigacji znajdziesz w artykule Zagnieżdżone wykresy.
Integracja z dolnym paskiem nawigacyjnym
Definiując NavController
na wyższym poziomie w hierarchii komponentów, możesz połączyć nawigację z innymi komponentami, np. z nawigacją u dołu. Dzięki temu możesz poruszać się po aplikacji, wybierając ikony na pasku u dołu.
Aby używać komponentów BottomNavigation
i BottomNavigationItem
, dodaj do aplikacji na Androida zależność androidx.compose.material
.
Groovy
dependencies { implementation "androidx.compose.material:material:1.8.3" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.8.3") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Aby połączyć elementy na pasku nawigacyjnym u dołu z trasami na wykresie nawigacji, zalecamy zdefiniowanie klasy, takiej jak TopLevelRoute
, która ma klasę trasy i ikonę.
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
Następnie umieść te trasy na liście, której może używać BottomNavigationItem
:
val topLevelRoutes = listOf(
TopLevelRoute("Profile", Profile, Icons.Profile),
TopLevelRoute("Friends", Friends, Icons.Friends)
)
W funkcji kompozycyjnej BottomNavigation
pobierz bieżący NavBackStackEntry
za pomocą funkcji currentBackStackEntryAsState()
. Ta pozycja zapewnia dostęp do bieżącej wersji NavDestination
. Wybrany stan każdego elementu BottomNavigationItem
można następnie określić, porównując ścieżkę elementu ze ścieżką bieżącego miejsca docelowego i jego nadrzędnych miejsc docelowych, aby obsługiwać przypadki, w których używasz zagnieżdżonej nawigacji za pomocą hierarchii NavDestination
.
Ścieżka elementu jest też używana do połączenia funkcji Lambda onClick
z wywołaniem funkcji navigate
, dzięki czemu po kliknięciu elementu użytkownik przechodzi do niego. Dzięki flagom saveState
i restoreState
stan i stos wsteczny tego elementu są prawidłowo zapisywane i przywracane podczas przełączania się między elementami nawigacji u dołu ekranu.
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(...) }
}
}
W tym przypadku korzystasz z metody NavController.currentBackStackEntryAsState()
, aby przenieść stan navController
z funkcji NavHost
i udostępnić go komponentowi BottomNavigation
. Oznacza to, że
BottomNavigation
automatycznie ma najbardziej aktualny stan.
Interoperacyjność
Jeśli chcesz używać komponentu Navigation z Compose, masz 2 możliwości:
- Zdefiniuj graf nawigacji za pomocą komponentu Navigation dla fragmentów.
- Zdefiniuj wykres nawigacji za pomocą
NavHost
w Compose, korzystając z miejsc docelowych Compose. Jest to możliwe tylko wtedy, gdy wszystkie ekrany w grafie nawigacji są funkcjami kompozycyjnymi.
Dlatego w przypadku aplikacji, które korzystają zarówno z Compose, jak i z widoków, zalecamy używanie komponentu nawigacji opartej na fragmentach. Fragmenty będą wtedy zawierać ekrany oparte na widokach, ekrany Compose i ekrany, które korzystają zarówno z widoków, jak i z Compose. Gdy zawartość każdego fragmentu znajdzie się w Compose, kolejnym krokiem będzie połączenie wszystkich ekranów za pomocą Navigation Compose i usunięcie wszystkich fragmentów.
Nawigacja z Compose za pomocą biblioteki Navigation dla fragmentów
Aby zmienić miejsca docelowe w kodzie Compose, udostępniasz zdarzenia, które mogą być przekazywane do dowolnego komponentu w hierarchii i przez niego wywoływane:
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
W fragmencie łączysz Compose z komponentem nawigacji opartym na fragmentach, wyszukując NavController
i przechodząc do miejsca docelowego:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
Możesz też przekazać NavController
w dół hierarchii Compose.
Udostępnianie prostych funkcji jest jednak bardziej przydatne i łatwiejsze do testowania.
Testowanie
Oddziel kod nawigacji od miejsc docelowych, które można komponować, aby umożliwić testowanie każdego z nich osobno, niezależnie od funkcji NavHost
.
Oznacza to, że nie należy przekazywać navController
bezpośrednio do żadnego komponentu, tylko przekazywać wywołania zwrotne nawigacji jako parametry. Dzięki temu wszystkie funkcje kompozycyjne można testować indywidualnie, ponieważ nie wymagają one w testach instancji navController
.
Poziom pośredniości zapewniany przez funkcję lambda composable
umożliwia oddzielenie kodu Navigation od samego komponentu. Działa to w 2 kierunkach:
- Przekazuj do funkcji kompozycyjnej tylko przeanalizowane argumenty
- Przekazuj lambdy, które powinny być wywoływane przez komponent kompozycyjny, aby nawigować, a nie samą funkcję
NavController
.
Na przykład funkcja kompozycyjna ProfileScreen
, która przyjmuje jako dane wejściowe userId
i umożliwia użytkownikom przejście na stronę profilu znajomego, może mieć następujący podpis:
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
Dzięki temu komponent ProfileScreen
działa niezależnie od Navigation, co umożliwia jego niezależne testowanie. Funkcja lambda composable
zawierałaby minimalną logikę potrzebną do połączenia interfejsów API Navigation z kompozycją:
@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))
}
}
Zalecamy pisanie testów, które obejmują wymagania dotyczące nawigacji w aplikacji, poprzez testowanie NavHost
, działań nawigacyjnych przekazywanych do komponentów kompozycyjnych, a także poszczególnych komponentów kompozycyjnych ekranu.
Testowanie NavHost
Aby rozpocząć testowanie NavHost
, dodaj tę zależność testowania nawigacji:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
Umieść NavHost
w aplikacji w funkcji kompozycyjnej, która przyjmuje NavHostController
jako parametr.
@Composable
fun AppNavHost(navController: NavHostController){
NavHost(navController = navController){ ... }
}
Teraz możesz przetestować AppNavHost
i całą logikę nawigacji zdefiniowaną w NavHost
, przekazując instancję artefaktu testowania nawigacji TestNavHostController
. Test interfejsu, który weryfikuje miejsce docelowe uruchomienia aplikacji i NavHost
, może wyglądać tak:
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()
}
}
Testowanie działań związanych z nawigacją
Implementację nawigacji możesz przetestować na kilka sposobów, klikając elementy interfejsu, a następnie weryfikując wyświetlane miejsce docelowe lub porównując oczekiwaną trasę z bieżącą.
Ponieważ chcesz przetestować implementację konkretnej aplikacji, preferowane są kliknięcia interfejsu. Aby dowiedzieć się, jak testować poszczególne funkcje kompozycyjne w izolacji, zapoznaj się z samouczkiem dotyczącym testowania w Jetpack Compose.
Możesz też użyć navController
, aby sprawdzić asercje, porównując bieżącą trasę z oczekiwaną za pomocą navController
:currentBackStackEntry
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
Więcej informacji o podstawach testowania w Compose znajdziesz w artykule Testowanie układu Compose i w samouczku Testowanie w Jetpack Compose. Więcej informacji o zaawansowanym testowaniu kodu nawigacji znajdziesz w przewodniku Testowanie nawigacji.
Więcej informacji
Więcej informacji o nawigacji Jetpack znajdziesz w artykule Pierwsze kroki z komponentem Navigation lub w samouczku dotyczącym nawigacji w Jetpack Compose.
Aby dowiedzieć się, jak zaprojektować nawigację w aplikacji, która dostosowuje się do różnych rozmiarów ekranu, orientacji i rodzajów urządzeń, przeczytaj artykuł Nawigacja w elastycznych interfejsach.
Aby dowiedzieć się więcej o bardziej zaawansowanej implementacji nawigacji w Compose w aplikacji modułowej, w tym o koncepcjach takich jak zagnieżdżone wykresy i integracja paska nawigacyjnego u dołu, zapoznaj się z aplikacją Now in Android na GitHubie.
Próbki
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony.
- Material Design 2 w Compose
- Migracja z Jetpack Navigation do Navigation Compose
- Gdzie przenieść stan