Navigation mit der Funktion „Schreiben“

Die Navigationskomponente unterstützt Jetpack Compose-Anwendungen. Sie können zwischen 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:

Groovig

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")
}

Erste Schritte

Bei der Implementierung der Navigation in einer App sollten Sie einen Navigationshost, eine Grafik und einen Controller implementieren. Weitere Informationen finden Sie in der Übersicht zur Navigation.

Informationen zum Erstellen eines NavController in Compose finden Sie unter Navigations-Controller erstellen im Abschnitt „Compose“.

NavHost erstellen

Informationen zum Erstellen einer NavHost in Compose finden Sie im Abschnitt „Compose“ unter Navigationsgrafik entwerfen.

Informationen zum Aufrufen einer zusammensetzbaren Funktion finden Sie in der Architekturdokumentation unter Ziel aufrufen.

Navigation Compose unterstützt auch die Übergabe von Argumenten zwischen zusammensetzbaren Zielen. Dazu müssen Sie Ihrer Route Argumentplatzhalter hinzufügen, ä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. Der Parameter arguments von composable() akzeptiert eine Liste von NamedNavArgument-Objekten. Sie können mit der Methode navArgument() schnell eine NamedNavArgument erstellen und dann die genaue type angeben:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

Sie sollten 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"))
}

Um das Argument an das Ziel zu übergeben, müssen Sie es an die Route anhängen, wenn Sie den navigate-Aufruf ausführen:

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, beim Navigieren keine komplexen Datenobjekte zu umgehen, sondern die unbedingt notwendigen Informationen wie eine eindeutige Kennung oder eine andere Art von ID als Argumente für Navigationsaktionen zu übergeben:

// 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. Wenn Sie nach der Navigation an Ihrem Ziel gelandet sind, können Sie mithilfe der übergebenen ID die erforderlichen Informationen aus der Single Source of Truth laden. Wenn Sie die Argumente in der ViewModel abrufen möchten, die für den Zugriff auf die Datenschicht verantwortlich sind, verwenden Sie den SavedStateHandle von 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)

// …

}

So lassen sich Datenverluste während Konfigurationsänderungen und Inkonsistenzen beim Aktualisieren oder Modifizieren des betreffenden Objekts vermeiden.

Eine ausführlichere Erläuterung, warum Sie es vermeiden sollten, komplexe Daten als Argumente zu übergeben, 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 Abfrageparametersyntax ("?argName={argName}") eingefügt werden.
  • Für sie muss ein defaultValue oder nullable = true festgelegt sein (wodurch der Standardwert implizit auf null festgelegt wird)

Das bedeutet, dass alle optionalen Argumente der Funktion composable() explizit als Liste hinzugefügt werden müssen:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Selbst wenn kein Argument an das Ziel übergeben wird, wird stattdessen der defaultValue, „user1234“, verwendet.

Die Struktur der Verarbeitung der Argumente über die Routen bedeutet, dass Ihre zusammensetzbaren Funktionen völlig unabhängig von der Navigation bleiben und daher wesentlich besser testbar sind.

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"))
}

Mit diesen Deeplinks können Sie eine URL, eine Aktion oder einen MIME-Typ mit einer zusammensetzbaren Funktion verknüpfen. Standardmäßig sind diese Deeplinks nicht für externe Apps sichtbar. Wenn du diese Deeplinks extern verfügbar machen möchtest, 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>

Die Navigation automatisch Deeplinks zu dieser zusammensetzbaren Funktion, 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/$id".toUri(),
    context,
    MyActivity::class.java
)

val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}

Sie können dieses deepLinkPendingIntent dann wie jede andere PendingIntent verwenden, um Ihre App am Ziel des Deeplinks zu öffnen.

Verschachtelte Navigation

Informationen zum Erstellen verschachtelter Navigationsgrafiken finden Sie unter Verschachtelte Grafiken.

Einbindung in die untere Navigationsleiste

Wenn Sie NavController auf einer höheren Ebene in Ihrer zusammensetzbaren Hierarchie definieren, können Sie „Navigation“ mit anderen Komponenten wie der unteren Navigationskomponente verbinden. Dazu wählen Sie einfach die Symbole in der unteren Leiste aus.

Fügen Sie die Abhängigkeit androidx.compose.material in Ihre Android-App ein, um die Komponenten BottomNavigation und BottomNavigationItem zu verwenden.

Groovig

dependencies {
    implementation "androidx.compose.material:material:1.6.5"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.11"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

dependencies {
    implementation("androidx.compose.material:material:1.6.5")
}

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.11"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Wenn Sie die Elemente in einer Navigationsleiste unten mit Routen in Ihrem Navigationsdiagramm verknüpfen möchten, sollten Sie eine versiegelte Klasse wie Screen definieren, 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, um Fälle zu verarbeiten, in denen 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 dieses Element aufgerufen wird. Mit den Flags saveState und restoreState werden Status und Back Stack dieses Elements korrekt gespeichert und wiederhergestellt, wenn Sie zwischen den unteren Navigationselementen wechseln.

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 navController-Status aus der NavHost-Funktion zu ziehen und mit der BottomNavigation-Komponente zu teilen. Das bedeutet, dass BottomNavigation automatisch den aktuellen 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 können Sie Abstürze vermeiden und Folgendes sicherstellen:

  • Die Argumente, die Sie beim Aufrufen eines Ziels oder einer Navigationsgrafik angeben, sind die richtigen Typen und alle erforderlichen Argumente müssen vorhanden sein.
  • Die Argumente, die Sie von SavedStateHandle abrufen, sind die richtigen Typen.

Weitere Informationen dazu finden Sie unter Type Safety in Kotlin DSL and Navigation Composer.

Interoperabilität

Wenn Sie die Komponente „Navigation“ zusammen mit der Funktion „Schreiben“ verwenden möchten, haben Sie zwei Möglichkeiten:

  • Definieren Sie mit der Komponente „Navigation“ für Fragmente ein Navigationsdiagramm.
  • Definieren Sie in der Funktion „Compose“ mithilfe von „Compose“-Zielen ein Navigationsdiagramm mit einem NavHost. Dies ist nur möglich, wenn alle Bildschirme im Navigationsdiagramm zusammensetzbar sind.

Daher wird für gemischte Anwendungen zum Schreiben und für Ansichten empfohlen, die fragmentbasierte Navigationskomponente zu verwenden. Die Fragmente enthalten dann ansichtsbasierte Bildschirme, Bildschirmen zum Verfassen und Bildschirme, die sowohl Ansichten als auch „Schreiben“ verwenden. Sobald sich die Inhalte jedes Fragments in der Erstellung befinden, verknüpfen Sie im nächsten Schritt alle diese Bildschirme mit der Funktion „Navigationskomposition“ und entfernen alle Fragmente.

Um Ziele innerhalb von Compose-Code zu ändern, stellen Sie Ereignisse bereit, die an jede zusammensetzbare Funktion in der Hierarchie übergeben und durch diese ausgelöst werden können:

@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
    Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}

In Ihrem Fragment erstellen Sie die Brücke zwischen Compose und der fragmentbasierten Navigationskomponente. Dazu suchen Sie nach NavController und rufen zum Ziel auf:

override fun onCreateView( /* ... */ ) {
    setContent {
        MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
    }
}

Alternativ können Sie die NavController in der Erstellungshierarchie nach unten übergeben. Die Bereitstellung einfacher Funktionen ist jedoch viel besser wiederverwendbar und testbar.

Testen

Entkoppeln Sie den Navigationscode von Ihren zusammensetzbaren Zielen, damit jede zusammensetzbare Funktion isoliert von der zusammensetzbaren Funktion NavHost getestet werden kann.

Das bedeutet, dass Sie navController nicht direkt an eine zusammensetzbare Funktion übergeben sollten, sondern Navigations-Callbacks als Parameter. Dadurch können alle zusammensetzbaren Funktionen einzeln getestet werden, da sie in Tests keine Instanz von navController benötigen.

Mithilfe der von der Lambda-Funktion composable gegebenen Indirektionsebene können Sie Ihren Navigationscode von der zusammensetzbaren Funktion selbst trennen. Dies funktioniert in zwei Richtungen:

  • Übergeben Sie nur geparste Argumente in Ihre zusammensetzbare Funktion.
  • Übergeben Sie Lambdas, die von der zusammensetzbaren Funktion ausgelöst werden sollen, und nicht von NavController selbst.

Eine zusammensetzbare Funktion Profile, die eine userId als Eingabe annimmt und Nutzern ermöglicht, zur Profilseite eines Freundes zu navigieren, könnte beispielsweise die folgende Signatur haben:

@Composable
fun Profile(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 …
}

Auf diese Weise funktioniert die zusammensetzbare Funktion Profile unabhängig von Navigation und kann unabhängig getestet werden. Das Lambda composable würde die Mindestlogik kapseln, die erforderlich ist, um die Lücke zwischen den Navigations-APIs und Ihrer 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 Anforderungen an die App-Navigation abdecken. Testen Sie dazu NavHost, die an Ihre zusammensetzbaren Funktionen sowie die einzelnen zusammensetzbaren Funktionen, die an die Navigation übergeben werden.

NavHost wird getestet

Fügen Sie die folgende Abhängigkeit von Navigationstests hinzu , um mit dem Testen von NavHost zu beginnen:

dependencies {
// ...
  androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
  // ...
}

Sie können Ihr NavHost-Testobjekt einrichten und eine Instanz der navController-Instanz an dieses übergeben. Dazu bietet das Navigationstestartefakt ein TestNavHostController. Ein UI-Test, mit dem das Startziel Ihrer App und NavHost bestätigt wird, sieht so aus:

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 die Navigationsimplementierung auf verschiedene Arten testen. Klicken Sie dazu auf die UI-Elemente und prüfen Sie dann entweder das angezeigte Ziel oder die erwartete Route mit der aktuellen Route.

Wenn du die Implementierung deiner konkreten App testen möchtest, sind Klicks auf die Benutzeroberfläche vorzugsweise zu empfehlen. Im Codelab Testen in Jetpack Compose erfahren Sie, wie Sie dies mit einzelnen zusammensetzbaren Funktionen isoliert testen.

Sie können auch den navController verwenden, um Ihre Assertions zu prüfen. Dazu vergleichen Sie die aktuelle Stringroute mit der erwarteten Route. Dazu verwenden Sie den currentBackStackEntry von navController:

@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 im Codelab zum Testen in Jetpack Compose. Weitere Informationen zum erweiterten Testen des Navigationscodes finden Sie im Leitfaden Testnavigation.

Weitere Informationen

Weitere Informationen zu Jetpack Navigation finden Sie unter Erste Schritte mit der Navigationskomponente oder im Codelab zu 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 UI.

Weitere Informationen zu einer erweiterten Implementierung der Compose-Navigation in einer modularen App, einschließlich Konzepten wie verschachtelter Grafiken und der Einbindung der unteren Navigationsleiste, finden Sie in der App Now in Android auf GitHub.

Produktproben