Die Navigationskomponente bietet Unterstützung für Jetpack Compose-Anwendungen. Sie können zwischen Composables wechseln und dabei die Infrastruktur und Funktionen der Navigationskomponente nutzen.
Die neueste Alpha-Navigationsbibliothek, die speziell für Compose entwickelt wurde, finden Sie in der Dokumentation zu Navigation 3.
Einrichten
Wenn Sie Compose unterstützen möchten, verwenden Sie die folgende Abhängigkeit in der Datei build.gradle
Ihres App-Moduls:
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") }
Erste Schritte
Wenn Sie die Navigation in einer App implementieren, müssen Sie einen Navigationshost, ein Navigationsdiagramm und einen Navigationscontroller implementieren. Weitere Informationen finden Sie in der Übersicht zur Navigation.
NavController erstellen
Informationen zum Erstellen eines NavController
in Compose finden Sie im Compose-Abschnitt von Navigationscontroller erstellen.
NavHost erstellen
Informationen zum Erstellen eines NavHost
in Compose finden Sie im Compose-Abschnitt von Navigationsdiagramm entwerfen.
Zu einem Composable navigieren
Informationen zum Navigieren zu einem Composable finden Sie in der Architekturdokumentation unter Zu einem Ziel navigieren.
Mit Argumenten navigieren
Informationen zum Übergeben von Argumenten zwischen zusammensetzbaren Zielen finden Sie im Compose-Abschnitt von Navigationsdiagramm entwerfen.
Komplexe Daten während der Navigation abrufen
Es wird dringend empfohlen, beim Navigieren keine komplexen Datenobjekte zu übergeben, sondern stattdessen die minimal erforderlichen Informationen, z. B. eine eindeutige Kennung oder eine andere Form von ID, als Argumente zu übergeben:
// Pass only the user ID when navigating to a new destination as argument
navController.navigate(Profile(id = "user1234"))
Komplexe Objekte sollten als Daten in einer einzigen Quelle der Wahrheit gespeichert werden, z. B. in der Datenschicht. Sobald Sie Ihr Ziel erreicht haben, können Sie die erforderlichen Informationen aus der zentralen Quelle abrufen, indem Sie die übergebene ID verwenden. Wenn Sie die Argumente in Ihrem ViewModel
abrufen möchten, das für den Zugriff auf die Datenschicht verantwortlich ist, verwenden Sie das SavedStateHandle
des 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)
// …
}
So lassen sich Datenverluste bei Konfigurationsänderungen und Inkonsistenzen vermeiden, wenn das betreffende Objekt aktualisiert oder geändert wird.
Eine ausführlichere Erklärung, warum Sie keine komplexen Daten als Argumente übergeben sollten, sowie eine Liste der unterstützten Argumenttypen finden Sie unter Daten zwischen Zielen übergeben.
Deeplinks
Navigation Compose unterstützt Deeplinks, die auch als Teil der Funktion composable()
definiert werden können. Der Parameter deepLinks
akzeptiert eine Liste von NavDeepLink
-Objekten, die schnell mit der Methode navDeepLink()
erstellt werden können:
@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)
}
Mit diesen Deep-Links können Sie eine bestimmte URL, Aktion oder einen bestimmten MIME-Typ mit einem Composable verknüpfen. Standardmäßig sind diese Deeplinks für externe Apps nicht verfügbar. Damit diese Deeplinks extern verfügbar sind, müssen Sie der manifest.xml
-Datei Ihrer App die entsprechenden <intent-filter>
-Elemente hinzufügen. Damit der Deeplink im vorherigen Beispiel funktioniert, 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>
Die Navigation verweist automatisch auf dieses Composable, wenn der Deeplink von einer anderen App ausgelöst wird.
Dieselben Deeplinks können auch verwendet werden, um ein 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/profile/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
Sie können diese deepLinkPendingIntent
dann wie jede andere PendingIntent
verwenden, um Ihre App am Deeplink-Ziel zu öffnen.
Geschachtelte Navigation
Informationen zum Erstellen verschachtelter Navigationsgraphen finden Sie unter Verschachtelte Graphen.
Einbindung in die untere Navigationsleiste
Wenn Sie NavController
auf einer höheren Ebene in Ihrer zusammensetzbaren Hierarchie definieren, können Sie die Navigation mit anderen Komponenten wie der Bottom-Navigation-Komponente verbinden. So können Sie durch Auswahl der Symbole in der unteren Leiste navigieren.
Wenn Sie die Komponenten BottomNavigation
und BottomNavigationItem
verwenden möchten, fügen Sie Ihrer Android-Anwendung die Abhängigkeit androidx.compose.material
hinzu.
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" } }
Wenn Sie die Elemente in einer unteren Navigationsleiste mit Routen in Ihrem Navigationsdiagramm verknüpfen möchten, empfiehlt es sich, eine Klasse wie TopLevelRoute
zu definieren, die eine Routenklasse und ein Symbol enthält.
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
Fügen Sie diese Routen dann in eine Liste ein, die von BottomNavigationItem
verwendet werden kann:
val topLevelRoutes = listOf(
TopLevelRoute("Profile", Profile, Icons.Profile),
TopLevelRoute("Friends", Friends, Icons.Friends)
)
Rufen Sie in Ihrer zusammensetzbaren Funktion BottomNavigation
den aktuellen NavBackStackEntry
mit der Funktion currentBackStackEntryAsState()
ab. Über diesen Eintrag haben Sie Zugriff auf den aktuellen NavDestination
. Der ausgewählte Status der einzelnen BottomNavigationItem
kann dann durch Vergleichen des Routenverlaufs des Elements mit dem Routenverlauf des aktuellen Ziels und seiner übergeordneten Ziele bestimmt werden, um Fälle zu verarbeiten, in denen Sie die geschachtelte Navigation mit der Hierarchie NavDestination
verwenden.
Die Route des Elements wird auch verwendet, um die onClick
-Lambda-Funktion mit einem Aufruf von navigate
zu verbinden, sodass durch Tippen auf das Element zu diesem Element navigiert wird. Durch die Verwendung der Flags saveState
und restoreState
werden der Status und der Backstack dieses Elements beim Wechseln zwischen Elementen der unteren Navigation korrekt gespeichert und wiederhergestellt.
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(...) }
}
}
Hier nutzen Sie die Methode NavController.currentBackStackEntryAsState()
, um den Status navController
aus der Funktion NavHost
zu heben und ihn mit der Komponente BottomNavigation
zu teilen. Das bedeutet, dass BottomNavigation
automatisch den aktuellen Status hat.
Interoperabilität
Wenn Sie die Navigation-Komponente mit Compose verwenden möchten, haben Sie zwei Möglichkeiten:
- Definieren Sie mit der Navigation-Komponente einen Navigationsgraphen für Fragmente.
- Definieren Sie mit Compose Destinations einen Navigationsgraphen mit einem
NavHost
in Compose. Das ist nur möglich, wenn alle Bildschirme im Navigationsdiagramm Composables sind.
Daher wird für Apps mit einer Mischung aus Compose und Views die Verwendung der fragmentbasierten Navigationskomponente empfohlen. Fragmente enthalten dann View-basierte Bildschirme, Compose-Bildschirme und Bildschirme, die sowohl Views als auch Compose verwenden. Sobald die Inhalte jedes Fragments in Compose sind, müssen Sie alle diese Bildschirme mit Navigation Compose verknüpfen und alle Fragmente entfernen.
Mit Navigation für Fragmente von Compose aus navigieren
Wenn Sie Ziele im Compose-Code ändern möchten, machen Sie Ereignisse verfügbar, die an ein beliebiges Composable in der Hierarchie übergeben und von diesem ausgelöst werden können:
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
In Ihrem Fragment stellen Sie die Verbindung zwischen Compose und der fragmentbasierten Navigationskomponente her, indem Sie das NavController
suchen und zum Ziel navigieren:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
Alternativ können Sie NavController
auch in Ihrer Compose-Hierarchie übergeben.
Das Bereitstellen einfacher Funktionen ist jedoch viel wiederverwendbarer und testbarer.
Testen
Entkoppeln Sie den Navigationscode von Ihren zusammensetzbaren Zielen, damit Sie jede zusammensetzbare Funktion isoliert und unabhängig von der zusammensetzbaren Funktion NavHost
testen können.
Das bedeutet, dass Sie die navController
nicht direkt an einComposable übergeben, sondern stattdessen Navigations-Callbacks als Parameter übergeben sollten. So können alle Ihre Composables einzeln getestet werden, da für Tests keine Instanz von navController
erforderlich ist.
Die Indirektionsebene, die durch die composable
-Lambda bereitgestellt wird, ermöglicht es Ihnen, Ihren Navigationscode vom Composable selbst zu trennen. Das funktioniert in zwei Richtungen:
- Übergeben Sie nur geparste Argumente an Ihre Composable-Funktion.
- Übergeben Sie Lambdas, die durch das Composable ausgelöst werden sollen, um zu navigieren, anstatt
NavController
selbst.
Ein Beispiel: Eine ProfileScreen
-Composable, die eine userId
als Eingabe akzeptiert und es Nutzern ermöglicht, zur Profilseite eines Freundes zu navigieren, könnte die folgende Signatur haben:
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
So funktioniert die zusammensetzbare Funktion ProfileScreen
unabhängig von der Navigation und kann unabhängig getestet werden. Das Lambda composable
kapselt die minimale Logik, die erforderlich ist, um die Lücke zwischen den Navigation APIs und Ihrer Composable zu schließen:
@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))
}
}
Es wird empfohlen, Tests zu schreiben, die die Navigationsanforderungen Ihrer App abdecken. Testen Sie dazu die NavHost
, die Navigationsaktionen, die an Ihre Composables übergeben werden, sowie Ihre einzelnen Screen-Composables.
NavHost
wird getestet
Fügen Sie die folgende Abhängigkeit für Navigationstests hinzu , um mit dem Testen von NavHost
zu beginnen:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
Schließen Sie das NavHost
Ihrer App in eine Composable ein, die ein NavHostController
als Parameter akzeptiert.
@Composable
fun AppNavHost(navController: NavHostController){
NavHost(navController = navController){ ... }
}
Jetzt können Sie AppNavHost
und die gesamte in NavHost
definierte Navigationslogik testen, indem Sie eine Instanz des Navigations-Testartefakts TestNavHostController
übergeben. Ein UI-Test, der das Startziel Ihrer App 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 vorzuziehen. Informationen zum Testen von einzelnen Composables finden Sie im Codelab Testing in Jetpack Compose.
Sie können die navController
auch verwenden, um Ihre Zusicherungen zu prüfen, indem Sie die aktuelle Route mit der erwarteten Route vergleichen. Verwenden Sie dazu die currentBackStackEntry
von navController
:
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
Weitere Informationen zu den Grundlagen von Compose-Tests finden Sie unter Compose-Layout testen und im Codelab zu Tests in Jetpack Compose. Weitere Informationen zum erweiterten Testen von Navigationscode finden Sie im Leitfaden Navigation testen.
Weitere Informationen
Weitere Informationen zur Jetpack Navigation-Komponente finden Sie unter Erste Schritte mit der Navigation-Komponente oder im Codelab zur Jetpack Compose-Navigation.
Informationen dazu, wie Sie die Navigation Ihrer App so gestalten, dass sie sich an verschiedene Bildschirmgrößen, Ausrichtungen und Formfaktoren anpasst, finden Sie unter Navigation für responsive UIs.
Informationen zu einer komplexeren Compose-Navigationsimplementierung in einer modularisierten App, einschließlich Konzepten wie verschachtelten Diagrammen und der Integration der unteren Navigationsleiste, finden Sie in der Now in Android-App auf GitHub.
Produktproben
Empfehlungen für dich
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Material Design 2 in Compose
- Jetpack Navigation zu Navigation Compose migrieren
- Wo wird der Status hochgeladen?