Navigation mit der Funktion „Schreiben“

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.

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.

Informationen zum Navigieren zu einem Composable finden Sie in der Architekturdokumentation unter Zu einem Ziel 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.

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.

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