Die Navigationskomponente bietet Unterstützung für Jetpack Composer-Anwendungen. Sie können zwischen den zusammensetzbaren Funktionen wechseln und gleichzeitig die Infrastruktur und Funktionen der Navigationskomponente nutzen.
Einrichten
Verwenden Sie die folgende Abhängigkeit in der Datei build.gradle
Ihres App-Moduls, um Compose zu unterstützen:
Groovy
dependencies { def nav_version = "2.7.7" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.7.7" implementation("androidx.navigation:navigation-compose:$nav_version") }
Jetzt starten
Wenn Sie die Navigation in einer App implementieren, implementieren Sie einen Navigationshost, eine Grafik und einen Controller. Weitere Informationen finden Sie in der Übersicht über die Navigation.
NavController erstellen
Informationen zum Erstellen eines NavController
in „Compose“ finden Sie im Abschnitt „Compose“ von Navigationscontroller erstellen.
NavHost erstellen
Informationen zum Erstellen eines NavHost
in „Compose“ finden Sie im Abschnitt „Compose“ des Artikels Navigationsdiagramm entwerfen.
Rufen Sie eine zusammensetzbare Funktion auf.
Weitere Informationen zum Aufrufen einer zusammensetzbaren Funktion finden Sie in der Architekturdokumentation unter Zu einem Ziel navigieren.
Mit Argumenten navigieren
Navigation Compose unterstützt auch die Übergabe von Argumenten zwischen zusammensetzbaren Zielen. Dazu müssen Sie der Route Argumentplatzhalter hinzufügen. Dies funktioniert ähnlich wie beim Hinzufügen von Argumenten zu einem Deeplink, wenn Sie die Basisnavigationsbibliothek verwenden:
NavHost(startDestination = "profile/{userId}") {
...
composable("profile/{userId}") {...}
}
Standardmäßig werden alle Argumente als Strings geparst. Für den Parameter arguments
von composable()
kann eine Liste mit NamedNavArgument
-Objekten angegeben werden. Sie können ein NamedNavArgument
schnell mit der Methode navArgument()
erstellen und dann seinen genauen type
angeben:
NavHost(startDestination = "profile/{userId}") {
...
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
}
Du solltest die Argumente aus dem NavBackStackEntry
extrahieren, das in der Lambda-Funktion der composable()
-Funktion verfügbar ist.
composable("profile/{userId}") { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
Damit das Argument an das Ziel übergeben werden kann, müssen Sie es beim Aufruf navigate
an die Route anhängen:
navController.navigate("profile/user1234")
Eine Liste der unterstützten Typen finden Sie unter Daten zwischen Zielen übergeben.
Komplexe Daten beim Navigieren abrufen
Es wird dringend empfohlen, bei der Navigation keine komplexen Datenobjekte zu übergeben, sondern die mindestens erforderlichen Informationen wie eine eindeutige Kennung oder eine andere Art von ID als Argumente bei Navigationsaktionen weiterzugeben:
// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")
Komplexe Objekte sollten als Daten in einer einzigen Datenquelle gespeichert werden, z. B. in der Datenschicht. Sobald Sie nach der Navigation am Ziel angekommen sind, können Sie die erforderlichen Informationen mithilfe der übergebenen ID aus der Single Source of Truth laden. Um die Argumente in der ViewModel
abzurufen, die für den Zugriff auf die Datenschicht verantwortlich ist, verwenden Sie den SavedStateHandle
des 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)
// …
}
Dadurch werden Datenverluste während Konfigurationsänderungen und Inkonsistenzen beim Aktualisieren oder Ändern des betreffenden Objekts verhindert.
Eine ausführlichere Erläuterung, warum Sie komplexe Daten nicht als Argumente übergeben sollten, sowie eine Liste der unterstützten Argumenttypen finden Sie unter Daten zwischen Zielen übergeben.
Optionale Argumente hinzufügen
Navigation Compose unterstützt auch optionale Navigationsargumente. Optionale Argumente unterscheiden sich in zweierlei Hinsicht von erforderlichen Argumenten:
- Sie müssen mithilfe der Syntax für Suchparameter (
"?argName={argName}"
) eingeschlossen werden. - Für sie muss ein
defaultValue
odernullable = true
festgelegt sein, wodurch der Standardwert implizit aufnull
gesetzt wird.
Das bedeutet, dass alle optionalen Argumente explizit als Liste zur Funktion composable()
hinzugefügt werden müssen:
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
Auch wenn jetzt kein Argument an das Ziel übergeben wird, wird stattdessen defaultValue
, „user1234“, verwendet.
Die Struktur der Verarbeitung der Argumente über die Routen bedeutet, dass Ihre zusammensetzbaren Funktionen vollständig unabhängig von der Navigation sind und viel testbarer sind.
Deeplinks
Navigation Compose unterstützt implizite Deeplinks, die auch als Teil der composable()
-Funktion definiert werden können. Der Parameter deepLinks
akzeptiert eine Liste von NavDeepLink
-Objekten, die schnell mit der Methode navDeepLink()
erstellt werden können:
val uri = "https://www.example.com"
composable(
"profile?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("id"))
}
Über diese Deeplinks können Sie eine bestimmte URL, Aktion oder einen MIME-Typ mit einer zusammensetzbaren Funktion verknüpfen. Standardmäßig sind diese Deeplinks nicht für externe Apps sichtbar. Wenn diese Deeplinks extern verfügbar sein sollen, musst du der Datei manifest.xml
deiner App die entsprechenden <intent-filter>
-Elemente hinzufügen. Um den Deeplink im vorherigen Beispiel zu aktivieren, müssen Sie Folgendes in das <activity>
-Element des Manifests einfügen:
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
Bei der Navigation werden automatisch Deeplinks zu dieser zusammensetzbaren Funktion erstellt, wenn der Deeplink von einer anderen App ausgelöst wird.
Dieselben Deeplinks können auch verwendet werden, um einen PendingIntent
mit dem entsprechenden Deeplink aus einer zusammensetzbaren Funktion zu erstellen:
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)
}
Sie können diesen deepLinkPendingIntent
dann wie jeden anderen PendingIntent
verwenden, um die App am Deeplink-Ziel zu öffnen.
Verschachtelte Navigation
Informationen zum Erstellen verschachtelter Navigationsdiagramme finden Sie unter Verschachtelte Grafiken.
Integration in die Navigationsleiste unten
Wenn Sie NavController
auf einer höheren Ebene in der zusammensetzbaren Hierarchie definieren, können Sie die Navigation mit anderen Komponenten wie der unteren Navigationskomponente verbinden. Wählen Sie dazu die Symbole in der unteren Leiste aus.
Füge deiner Android-App die androidx.compose.material
-Abhängigkeit hinzu, um die Komponenten BottomNavigation
und BottomNavigationItem
zu verwenden.
Groovig
dependencies { implementation "androidx.compose.material:material:1.6.8" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.6.8") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Wenn Sie die Elemente in einer unteren Navigationsleiste mit Routen in Ihrem Navigationsdiagramm verknüpfen möchten, sollten Sie eine versiegelte Klasse definieren (z. B. Screen
), die die Route und die String-Ressourcen-ID für die Ziele enthält.
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)
}
Fügen Sie diese Elemente dann in eine Liste ein, die vom BottomNavigationItem
verwendet werden kann:
val items = listOf(
Screen.Profile,
Screen.FriendsList,
)
Rufen Sie in der zusammensetzbaren Funktion BottomNavigation
den aktuellen NavBackStackEntry
mit der Funktion currentBackStackEntryAsState()
ab. Mit diesem Eintrag erhalten Sie Zugriff auf den aktuellen NavDestination
. Der ausgewählte Status jedes BottomNavigationItem
kann dann ermittelt werden, indem die Route des Elements mit der Route des aktuellen Ziels und seiner übergeordneten Ziele verglichen wird. So lassen sich Fälle bearbeiten, wenn Sie die verschachtelte Navigation mit der NavDestination
-Hierarchie verwenden.
Die Route des Elements wird auch verwendet, um das Lambda onClick
mit einem Aufruf von navigate
zu verbinden, sodass durch Tippen auf das Element zu diesem Element navigiert wird. Mithilfe der Flags saveState
und restoreState
werden der Status und Back-Stack dieses Elements beim Wechseln zwischen den unteren Navigationselementen korrekt gespeichert und wiederhergestellt.
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) }
}
}
Hier nutzen Sie die Methode NavController.currentBackStackEntryAsState()
, um den Zustand navController
aus der Funktion NavHost
herauszuziehen und mit der Komponente BottomNavigation
zu teilen. Das bedeutet, dass BottomNavigation
automatisch den neuesten Status hat.
Sicherheit in „Navigation Compose“ eingeben
Der Code auf dieser Seite ist nicht typsicher. Sie können die Funktion navigate()
mit nicht vorhandenen Routen oder falschen Argumenten aufrufen. Sie können Ihren Navigationscode jedoch so strukturieren, dass er zur Laufzeit typsicher ist. So vermeiden Sie Abstürze und sorgen für Folgendes:
- Die Argumente, die Sie beim Aufrufen eines Ziels oder eines Navigationsdiagramms angeben, sind die richtigen Typen und alle erforderlichen Argumente sind vorhanden.
- Die Argumente, die Sie von
SavedStateHandle
abrufen, sind die richtigen Typen.
Weitere Informationen hierzu finden Sie unter Typsicherheit in Kotlin DSL und Navigation Compose.
Interoperabilität
Wenn Sie die Komponente „Navigation“ zusammen mit „Schreiben“ verwenden möchten, haben Sie zwei Möglichkeiten:
- Definieren Sie ein Navigationsdiagramm mit der Navigationskomponente für Fragmente.
- Mithilfe von „Compose“-Zielen können Sie mit einem
NavHost
in „Compose“ ein Navigationsdiagramm definieren. Dies ist nur möglich, wenn alle Bildschirme im Navigationsdiagramm zusammensetzbar sind.
Daher wird für gemischte Apps vom Typ „Schreiben“ und „Views“ empfohlen, die Komponente „Fragmentbasierte Navigation“ zu verwenden. Die Fragmente enthalten dann ansichtsbasierte Bildschirme, Erstellungsbildschirme und Bildschirme, die sowohl „Ansichten“ als auch „Schreiben“ verwenden. Sobald sich der Inhalt jedes Fragments in „Compose“ befindet, besteht der nächste Schritt darin, alle Bildschirme über „Navigation Compose“ miteinander zu verknüpfen und alle Fragmente zu entfernen.
Von „Schreiben mit Navigation“ für Fragmente wechseln
Um Ziele innerhalb des Compose-Codes zu ändern, stellen Sie Ereignisse bereit, die an jede zusammensetzbare Funktion in der Hierarchie übergeben und ausgelöst werden können:
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
In Ihrem Fragment stellen Sie die Brücke zwischen „Compose“ und der fragmentierten Navigationskomponente. Dazu suchen Sie nach NavController
und gehen zum Ziel:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
Alternativ können Sie das NavController
-Element in der Erstellungshierarchie nach unten übergeben.
Einfache Funktionen verfügbar zu machen, ist jedoch viel besser wiederverwendbar und testbar.
Testen
Entkoppeln Sie den Navigationscode von den zusammensetzbaren Zielen, um jede zusammensetzbare Funktion getrennt von der zusammensetzbaren Funktion NavHost
zu testen.
Sie sollten das navController
also nicht direkt an eine zusammensetzbare Funktion übergeben, sondern Navigations-Callbacks als Parameter übergeben. Dadurch können alle zusammensetzbaren Funktionen einzeln testbar sein, da für sie keine Instanz von navController
in Tests erforderlich ist.
Mit dem Grad der Indirektion, das die composable
Lambda-Funktion liefert, können Sie Ihren Navigationscode von der zusammensetzbaren Funktion selbst trennen. Dies funktioniert in zwei Richtungen:
- Nur geparste Argumente an die zusammensetzbare Funktion übergeben
- Übergeben Sie Lambdas, die von der zusammensetzbaren Funktion für die Navigation ausgelöst werden sollen, und nicht die
NavController
selbst.
Eine zusammensetzbare Funktion Profile
, die eine userId
als Eingabe annimmt und Nutzern ermöglicht, die Profilseite eines Freundes aufzurufen, könnte beispielsweise folgende Signatur haben:
@Composable
fun Profile(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
Auf diese Weise arbeitet die zusammensetzbare Funktion Profile
unabhängig von Navigation und kann unabhängig getestet werden. Die Lambda-Funktion composable
würde die minimale Logik kapseln, die erforderlich ist, um die Lücke zwischen den Navigation APIs und der zusammensetzbaren Funktion zu schließen:
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
navController.navigate("profile?userId=$friendUserId")
}
}
Es wird empfohlen, Tests zu schreiben, die Ihre App-Navigationsanforderungen abdecken. Testen Sie dazu NavHost
, die an Ihre zusammensetzbaren Funktionen übergebenen Navigationsaktionen und die einzelnen zusammensetzbaren Funktionen auf dem Bildschirm.
NavHost
wird getestet
Füge die folgende Abhängigkeit für Navigationstests hinzu , um mit dem Testen von NavHost
zu beginnen:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
Sie können das Testobjekt NavHost
einrichten und eine Instanz der Instanz navController
an dieses übergeben. Dazu stellt das Navigationstestartefakt ein TestNavHostController
bereit. Ein UI-Test, der das Startziel Ihrer Anwendung und NavHost
überprüft, würde so aussehen:
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()
}
}
Navigationsaktionen testen
Sie können Ihre Navigationsimplementierung auf verschiedene Arten testen, indem Sie auf die UI-Elemente klicken und dann entweder das angezeigte Ziel überprüfen oder die erwartete Route mit der aktuellen Route vergleichen.
Da Sie die Implementierung Ihrer konkreten App testen möchten, sind Klicks auf die Benutzeroberfläche besser geeignet. Im Codelab Testen in Jetpack Compose können Sie nachlesen, wie Sie dies mit einzelnen zusammensetzbaren Funktionen isoliert testen.
Sie können auch navController
verwenden, um Ihre Assertions zu prüfen. Dazu vergleichen Sie die aktuelle Stringroute mithilfe des currentBackStackEntry
von navController
mit der erwarteten Route:
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
val route = navController.currentBackStackEntry?.destination?.route
assertEquals(route, "profiles")
}
Weitere Informationen zu den Grundlagen von Compose-Tests finden Sie unter Compose-Layout testen und Testen in Jetpack Compose. Weitere Informationen zum erweiterten Testen von Navigationscode finden Sie im Leitfaden Navigation testen.
Weitere Informationen
Weitere Informationen zu Jetpack Navigation finden Sie unter Erste Schritte mit der Komponente „Navigation“ oder im Codelab zu Jetpack Composer Navigation.
Informationen dazu, wie du die App-Navigation so gestaltest, dass sie sich an verschiedene Bildschirmgrößen, Ausrichtungen und Formfaktoren anpasst, findest du unter Navigation für responsive UIs.
Weitere Informationen über eine erweiterte Compose-Navigationsimplementierung in einer modularen App, einschließlich Konzepten wie verschachtelten Grafiken und der Einbindung von unteren Navigationsleisten, finden Sie in der App Now in Android auf GitHub.
Produktproben
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Material Design 2 in Compose
- Jetpack Navigation zu Navigation Compose migrieren
- Wo soll der Windenstatus hergestellt werden?