Navigation mit der Funktion „Schreiben“

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.

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.

Weitere Informationen zum Aufrufen einer zusammensetzbaren Funktion finden Sie in der Architekturdokumentation unter Zu einem Ziel 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 oder nullable = true festgelegt sein, wodurch der Standardwert implizit auf null 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.

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.14"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

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

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.14"
    }

    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.

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