Komponent Nawigacja obsługuje aplikacje Jetpack Compose. Możesz przechodzić między elementami kompozycyjnymi, korzystając jednocześnie z infrastruktury i funkcji komponentu Nawigacja.
Konfiguracja
Aby obsługiwać Compose, użyj w pliku build.gradle
modułu aplikacji tej zależności:
Odlotowe
dependencies { def nav_version = "2.8.0" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.8.0" implementation("androidx.navigation:navigation-compose:$nav_version") }
Rozpocznij
Podczas implementowania nawigacji w aplikacji zaimplementuj hosta, wykres i sterownik nawigacji. Więcej informacji znajdziesz w omówieniu Nawigacji.
Tworzenie kontrolera nawigacji
Informacje o tworzeniu elementu NavController
w sekcji „Compose” (Utwórz) znajdziesz w sekcji Tworzenie kontrolera nawigacji.
Tworzenie hosta NavHost
Informacje o tworzeniu NavHost
w sekcji Kompozycja znajdziesz w artykule Projektowanie grafu nawigacji.
Przejdź do komponentu.
Informacje o nawigowaniu do komponentu znajdziesz w sekcji Nawigowanie do miejsca docelowego w dokumentacji architektury.
Poruszanie się za pomocą argumentów
Informacje o przekazywaniu argumentów między miejscami docelowymi do łączenia znajdziesz w sekcji „Tworzenie” w artykule Projektowanie wykresu nawigacji.
Pobieranie złożonych danych podczas nawigacji
Zdecydowanie zalecamy, aby podczas nawigacji nie przekazywać skomplikowanych obiektów danych, a zamiast tego przekazywać jako argumenty podczas wykonywania działań nawigacyjnych minimalną liczbę 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 prawdy, np. w warstwie danych. Po przejściu do miejsca docelowego możesz wczytać wymagane informacje z jednego źródła danych, korzystając z przekazanego identyfikatora. Aby pobrać w obiekcie ViewModel
argumenty odpowiedzialne za dostęp do warstwy danych, użyj funkcji SavedStateHandle
klasy 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 zmian konfiguracji i wszelkim niespójnościom związanym z aktualizacją lub mutacją obiektu, którego dotyczy dany problem.
Pełniejsze wyjaśnienie, dlaczego nie należy przekazywać złożonych danych jako argumentów, oraz lista obsługiwanych typów argumentów znajdują się w artykule Przesyłanie danych między miejscami docelowymi.
Precyzyjne linki
Funkcja tworzenia nawigacji obsługuje precyzyjne linki, które można zdefiniować w ramach funkcji composable()
. Jego parametr deepLinks
przyjmuje 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)
}
Dzięki tym precyzyjnym linkom możesz powiązać określony adres URL, działanie lub typ mime z komponewalnym. Domyślnie te precyzyjne linki nie są widoczne dla aplikacji zewnętrznych. 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 z poprzedniego przykładu, w elemencie <activity>
w pliku manifestu dodaj:
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
Nawigacja automatycznie tworzy precyzyjny link do tej składowej, gdy precyzyjny link zostanie wywołany przez inną aplikację.
Tych samych precyzyjnych linków możesz też używać do tworzenia PendingIntent
z odpowiednim precyzyjnym linkiem z komponowalnego:
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 potem użyć tego deepLinkPendingIntent
tak samo jak każdego innego PendingIntent
, aby otworzyć aplikację w miejscu docelowym precyzyjnego linku.
Nawigacja zagnieżdżona
Informacje o tworzeniu zagnieżdżonych diagramów nawigacji znajdziesz w artykule Zagnieżdżone diagramy.
Integracja z dolnym paskiem nawigacyjnym
Definiując NavController
na wyższym poziomie hierarchii komponentów, możesz połączyć komponent Nawigacja z innymi komponentami, takimi jak komponent dolnej nawigacji. Umożliwia to poruszanie się po ikonach na dolnym pasku.
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.7.5" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.7.5") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Aby połączyć elementy na dolnym pasku nawigacji z trasami na diagramie 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 komponentach BottomNavigation
pobieraj bieżącą wartość 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 trasę elementu z trasą bieżącego miejsca docelowego i jego nadrzędnych miejsc docelowych, aby obsłużyć przypadki, gdy korzystasz z nawierającej nawigacji w hierarchii NavDestination
.
Trasa elementu jest też używana do łączenia funkcji lambda onClick
z wywołaniem funkcji navigate
, dzięki czemu kliknięcie elementu powoduje przejście do tego elementu. Dzięki flagom saveState
i restoreState
stan i stos wsteczny elementu są prawidłowo zapisywane i przywracane podczas przełączania się między dolnymi elementami nawigacji.
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(...) }
}
}
Tutaj korzystasz z metody NavController.currentBackStackEntryAsState()
, aby wyodrębnić stan navController
z funkcji NavHost
i przekazać go komponentowi BottomNavigation
. Oznacza to, że BottomNavigation
automatycznie ma najbardziej aktualny stan.
Interoperacyjność
Jeśli chcesz używać komponentu Nawigacja z opcją tworzenia wiadomości, masz 2 możliwości:
- Zdefiniuj graf nawigacji za pomocą komponentu Nawigacja dla fragmentów.
- Zdefiniuj graf nawigacji za pomocą elementu
NavHost
w sekcji Compose (Tworzenie) przy użyciu miejsc docelowych Compose. Jest to możliwe tylko wtedy, gdy wszystkie ekrany na wykresie nawigacyjnym są kompozycyjne.
Dlatego w przypadku aplikacji mieszanych, które łączą komponenty Compose i View, zalecamy używanie komponentu nawigacji opartej na fragmentach. Fragmenty będą zawierać ekrany oparte na widokach, ekrany Compose oraz ekrany, które korzystają zarówno z widoków, jak i Compose. Gdy zawartość każdego fragmentu znajdzie się w Compose, należy połączyć wszystkie te ekrany z Compose nawigacji i usunąć wszystkie fragmenty.
Poruszanie się w Compose za pomocą nawigacji fragmentów
Aby zmienić miejsca docelowe w kodzie usługi Compose, musisz ujawnić zdarzenia, które mogą być przekazywane do dowolnego komponentu w hierarchii i mogą być przez niego wywoływane:
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
W fragmentie łączysz komponent Nawigacja oparty na fragmencie z komponentem Compose. Aby to zrobić, znajdź element NavController
i przejdź do miejsca docelowego:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
Możesz też przekazać NavController
w hierarchii Compose.
Jednak udostępnianie prostych funkcji jest znacznie bardziej przydatne i łatwe do przetestowania.
Testowanie
Oddziel kod nawigacyjny od miejsc docelowych kompozycyjnych, aby umożliwić testowanie każdej funkcji kompozycyjnej oddzielnie, niezależnie od funkcji kompozycyjnej NavHost
.
Oznacza to, że nie należy przekazywać funkcji navController
bezpośrednio do funkcji kompozycyjnej. Zamiast tego jako parametry przekazuj wywołania zwrotne nawigacji. Dzięki temu możesz osobno testować wszystkie elementy kompozycyjne, ponieważ w testach nie jest wymagane wystąpienie navController
.
Poziom pośrednictwa zapewniany przez funkcję lambda composable
pozwala oddzielić kod nawigacji od samego komponentu. Funkcja ta działa w 2 kierunkach:
- Przekazywanie do kompozytowego tylko przeanalizowanych argumentów
- Przekazywanie funkcji lambda, które powinny być wywoływane przez komponent, do nawigacji, a nie do
NavController
.
Na przykład kompozyt ProfileScreen
, który przyjmuje jako dane wejściowe userId
i pozwala użytkownikom przejść na stronę profilu znajomego, może mieć podpis:
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
Dzięki temu funkcja kompozycyjna ProfileScreen
działa niezależnie od Nawigacji, co umożliwia testowanie jej niezależnie. Funkcja lambda composable
ujęłaby minimalną logikę potrzebną do połączenia interfejsów API nawigacji z komponowalnym:
@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 napisanie testów, które obejmują wymagania dotyczące nawigacji w aplikacji. W tym celu przetestuj NavHost
, czyli czynności nawigacyjne przekazywane do komponentów, a także poszczególne komponenty ekranu.
Testowanie NavHost
Aby rozpocząć testowanie elementu NavHost
, dodaj tę zależność testowania nawigacji:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
Zawiń funkcję NavHost
aplikacji w komponent, który akceptuje jako parametr funkcję NavHostController
.
@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 UI, który sprawdza miejsce docelowe na początku NavHost
, 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 testować na kilka sposobów: klikając elementy interfejsu, a następnie weryfikując wyświetlone miejsce docelowe lub porównując oczekiwaną trasę z obecną.
Ponieważ chcesz przetestować implementację konkretnej aplikacji, kliknięcia w interfejsie są preferowane. Aby dowiedzieć się, jak przetestować tę funkcję wraz z osobnymi funkcjami kompozycyjnymi, zapoznaj się z ćwiczeniem w Codelabs dotyczącym testowania w Jetpack Compose.
Możesz też użyć navController
, aby sprawdzić swoje twierdzenia, porównując bieżącą trasę z oczekiwaną, korzystając z funkcji navController
:currentBackStackEntry
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
Więcej wskazówek dotyczących podstaw testowania w Compose znajdziesz w artykule Testowanie układu Compose i w codelab Testowanie w Jetpack Compose. Więcej informacji o zaawansowanym testowaniu kodu nawigacji znajdziesz w przewodniku Testowanie nawigacji.
Więcej informacji
Więcej informacji o komponencie Nawigacja w Jetpacku znajdziesz w artykule Pierwsze kroki z komponentem Nawigacja lub w laboratorium kodu Nawigacja w Jetpack Compose.
Aby dowiedzieć się, jak zaprojektować nawigację w aplikacji w taki sposób, aby dostosowywała się do różnych rozmiarów ekranu, orientacji i formatów, przeczytaj artykuł Nawigacja po elastycznych interfejsach.
Aby dowiedzieć się więcej o bardziej zaawansowanej implementacji nawigacji w Compose w ramach aplikacji modułowej, w tym o takich koncepcjach jak zintegrowane wykresy i pasek nawigacji u dołu ekranu, zapoznaj się z aplikacją Now in Android na GitHubie.
Próbki
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Material Design 2 w Compose
- Migracja Jetpack Navigation do Navigation Compose
- Gdzie umieścić stan