Navigation 3 bietet ein leistungsstarkes und flexibles System zum Verwalten des UI-Ablaufs Ihrer App über Szenen. Mit Szenen können Sie hochgradig angepasste Layouts erstellen, sich an verschiedene Bildschirmgrößen anpassen und komplexe Multi-Pane-Anwendungen nahtlos verwalten.
Szenen
In Navigation 3 ist ein Scene die Grundeinheit, mit der eine oder mehrere NavEntry-Instanzen gerendert werden. Stellen Sie sich ein Scene als einen separaten visuellen Zustand oder Abschnitt Ihrer Benutzeroberfläche vor, der die Anzeige von Inhalten aus Ihrem Backstack enthalten und verwalten kann.
Jede Scene-Instanz wird eindeutig durch ihren key und die Klasse des Scene selbst identifiziert. Diese eindeutige Kennung ist wichtig, da sie die Animation auf oberster Ebene steuert, wenn sich Scene ändert.
Die Scene-Schnittstelle hat die folgenden Eigenschaften:
key: Any: Eine eindeutige Kennung für diese spezielleScene-Instanz. Dieser Schlüssel sorgt in Kombination mit der Klasse desScenefür die Unterscheidung, hauptsächlich für Animationszwecke.entries: List<NavEntry<T>>: Eine Liste vonNavEntry-Objekten, die vomSceneangezeigt werden. Wichtig: Wenn dasselbeNavEntrywährend eines Übergangs in mehrerenScenesangezeigt wird (z.B. bei einem Übergang mit gemeinsam genutzten Elementen), werden die Inhalte nur vom letzten Ziel-Scenegerendert, in dem sie angezeigt werden.previousEntries: List<NavEntry<T>>: Diese Property definiert dieNavEntrys, die sich ergeben, wenn von der aktuellenSceneaus eine „Zurück“-Aktion erfolgt. Dies ist wichtig, um den richtigen vorherigen Zustand für die Vorhersage zu berechnen. So kannNavDisplayden richtigen vorherigen Zustand, der möglicherweise eine Szene mit einer anderen Klasse und/oder einem anderen Schlüssel ist, vorhersagen und zu ihm wechseln.content: @Composable () -> Unit: Dies ist die zusammensetzbare Funktion, in der Sie definieren, wie dasSceneseinentriesund alle umgebenden UI-Elemente rendert, die für diesesScenespezifisch sind.
Szenenstrategien verstehen
Ein SceneStrategy ist der Mechanismus, der bestimmt, wie eine bestimmte Liste von NavEntrys aus dem Backstack angeordnet und in ein Scene überführt werden soll. Wenn ein SceneStrategy die aktuellen Backstack-Einträge erhält, stellt es sich im Wesentlichen zwei wichtige Fragen:
- Kann ich aus diesen Einträgen eine
Sceneerstellen? Wenn dieSceneStrategyfeststellt, dass sie die angegebenenNavEntryverarbeiten und eine sinnvolleScene(z.B. einen Dialog oder ein Layout mit mehreren Bereichen) erstellen kann, wird fortgefahren. Andernfalls wirdnullzurückgegeben, damit andere Strategien die Möglichkeit haben, einenScenezu erstellen. - Falls ja, wie sollte ich diese Einträge in der
Scene?anordnen? Sobald sich einSceneStrategydazu verpflichtet, die Einträge zu verarbeiten, übernimmt es die Verantwortung für die Erstellung einerSceneund die Definition der Darstellung der angegebenenNavEntrys in dieserScene.
Das Herzstück eines SceneStrategy ist die Methode calculateScene:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
Diese Methode ist eine Erweiterungsfunktion für ein SceneStrategyScope, das das aktuelle List<NavEntry<T>> aus dem Backstack verwendet. Die Methode sollte Scene<T> zurückgeben, wenn sie aus den bereitgestellten Einträgen erfolgreich eine bilden kann, andernfalls null.
Die SceneStrategyScope ist für die Verwaltung aller optionalen Argumente verantwortlich, die die SceneStrategy möglicherweise benötigt, z. B. einen onBack-Callback.
SceneStrategy bietet auch eine praktische Infix-Funktion then, mit der Sie mehrere Strategien verketten können. So wird eine flexible Pipeline für die Entscheidungsfindung erstellt, in der jede Strategie versucht, einen Scene zu berechnen. Wenn das nicht möglich ist, wird die Aufgabe an die nächste Strategie in der Kette delegiert.
Zusammenwirken von Szenen und Szenenstrategien
Die NavDisplay ist die zentrale zusammensetzbare Funktion, die Ihren Backstack beobachtet und mithilfe eines SceneStrategy die entsprechende Scene bestimmt und rendert.
Für den Parameter NavDisplay's sceneStrategy ist ein SceneStrategy erforderlich, mit dem der anzuzeigende Scene berechnet wird. Wenn mit der angegebenen Strategie (oder Strategiekette) kein Scene berechnet wird, wird für NavDisplay standardmäßig automatisch ein SinglePaneSceneStrategy verwendet.
So läuft die Interaktion ab:
- Wenn Sie dem Backstack Schlüssel hinzufügen oder daraus entfernen (z.B. mit
backStack.add()oderbackStack.removeLastOrNull()), werden diese Änderungen vonNavDisplaybeobachtet. - Die
NavDisplayübergibt die aktuelle Liste derNavEntrys(abgeleitet von den Backstack-Schlüsseln) an die konfigurierteSceneStrategy's calculateScene-Methode. - Wenn
SceneStrategyerfolgreich eineScenezurückgibt, rendertNavDisplaydiecontentdieserScene. DasNavDisplayverwaltet auch Animationen und die Vorhersage für die Zurück-Geste basierend auf den Attributen desScene.
Beispiel: Layout mit einem Bereich (Standardverhalten)
Das einfachste benutzerdefinierte Layout ist eine Einzelbereichsanzeige. Dies ist das Standardverhalten, wenn keine andere SceneStrategy Vorrang hat.
data class SinglePaneScene<T : Any>( override val key: Any, val entry: NavEntry<T>, override val previousEntries: List<NavEntry<T>>, ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(entry) override val content: @Composable () -> Unit = { entry.Content() } } /** * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the * list. */ public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> { override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? = SinglePaneScene( key = entries.last().contentKey, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
Beispiel: Einfaches Layout mit zwei Bereichen (benutzerdefinierte Szene und Strategie)
In diesem Beispiel wird gezeigt, wie Sie ein einfaches Layout mit zwei Bereichen erstellen, das auf Grundlage von zwei Bedingungen aktiviert wird:
- Die Fensterbreite ist ausreichend, um zwei Bereiche zu unterstützen (d.h. mindestens
WIDTH_DP_MEDIUM_LOWER_BOUND). - Die beiden obersten Einträge im Backstack deklarieren explizit ihre Unterstützung für die Anzeige in einem Layout mit zwei Bereichen mithilfe bestimmter Metadaten.
Das folgende Snippet ist der kombinierte Quellcode für TwoPaneScene.kt und TwoPaneSceneStrategy.kt:
// --- TwoPaneScene --- /** * A custom [Scene] that displays two [NavEntry]s side-by-side in a 50/50 split. */ class TwoPaneScene<T : Any>( override val key: Any, override val previousEntries: List<NavEntry<T>>, val firstEntry: NavEntry<T>, val secondEntry: NavEntry<T> ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(firstEntry, secondEntry) override val content: @Composable (() -> Unit) = { Row(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.weight(0.5f)) { firstEntry.Content() } Column(modifier = Modifier.weight(0.5f)) { secondEntry.Content() } } } companion object { internal const val TWO_PANE_KEY = "TwoPane" /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * in a two-pane layout. */ fun twoPane() = mapOf(TWO_PANE_KEY to true) } } @Composable fun <T : Any> rememberTwoPaneSceneStrategy(): TwoPaneSceneStrategy<T> { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass return remember(windowSizeClass) { TwoPaneSceneStrategy(windowSizeClass) } } // --- TwoPaneSceneStrategy --- /** * A [SceneStrategy] that activates a [TwoPaneScene] if the window is wide enough * and the top two back stack entries declare support for two-pane display. */ class TwoPaneSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> { override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? { // Condition 1: Only return a Scene if the window is sufficiently wide to render two panes. // We use isWidthAtLeastBreakpoint with WIDTH_DP_MEDIUM_LOWER_BOUND (600dp). if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { return null } val lastTwoEntries = entries.takeLast(2) // Condition 2: Only return a Scene if there are two entries, and both have declared // they can be displayed in a two pane scene. return if (lastTwoEntries.size == 2 && lastTwoEntries.all { it.metadata.containsKey(TwoPaneScene.TWO_PANE_KEY) } ) { val firstEntry = lastTwoEntries.first() val secondEntry = lastTwoEntries.last() // The scene key must uniquely represent the state of the scene. val sceneKey = Pair(firstEntry.contentKey, secondEntry.contentKey) TwoPaneScene( key = sceneKey, // Where we go back to is a UX decision. In this case, we only remove the top // entry from the back stack, despite displaying two entries in this scene. // This is because in this app we only ever add one entry to the // back stack at a time. It would therefore be confusing to the user to add one // when navigating forward, but remove two when navigating back. previousEntries = entries.dropLast(1), firstEntry = firstEntry, secondEntry = secondEntry ) } else { null } } }
Wenn Sie diese TwoPaneSceneStrategy in Ihrem NavDisplay verwenden möchten, müssen Sie Ihre entryProvider-Aufrufe so ändern, dass sie TwoPaneScene.twoPane()-Metadaten für die Einträge enthalten, die in einem Layout mit zwei Bereichen angezeigt werden sollen. Geben Sie dann TwoPaneSceneStrategy() als sceneStrategy an und verlassen Sie sich auf den Standard-Fallback für Szenarien mit einem Bereich:
// Define your navigation keys @Serializable data object ProductList : NavKey @Serializable data class ProductDetail(val id: String) : NavKey @Composable fun MyAppContent() { val backStack = rememberNavBackStack(ProductList) NavDisplay( backStack = backStack, entryProvider = entryProvider { entry<ProductList>( // Mark this entry as eligible for two-pane display metadata = TwoPaneScene.twoPane() ) { key -> Column { Text("Product List") Button(onClick = { backStack.add(ProductDetail("ABC")) }) { Text("View Details for ABC (Two-Pane Eligible)") } } } entry<ProductDetail>( // Mark this entry as eligible for two-pane display metadata = TwoPaneScene.twoPane() ) { key -> Text("Product Detail: ${key.id} (Two-Pane Eligible)") } // ... other entries ... }, // Simply provide your custom strategy. NavDisplay will fall back to SinglePaneSceneStrategy automatically. sceneStrategy = rememberTwoPaneSceneStrategy(), onBack = { if (backStack.isNotEmpty()) { backStack.removeLastOrNull() } } ) }
Listendetailinhalte in einer adaptiven Material-Szene anzeigen
Für den Anwendungsfall „Listendetails“ stellt das androidx.compose.material3.adaptive:adaptive-navigation3-Artefakt ein ListDetailSceneStrategy bereit, mit dem eine Scene mit Listendetails erstellt wird. Diese Scene übernimmt automatisch die Verarbeitung komplexer Anordnungen mit mehreren Bereichen (Liste, Detail und zusätzliche Bereiche) und passt sie an die Fenstergröße und den Gerätestatus an.
So erstellen Sie eine Materiallistendetail-Scene:
- Abhängigkeit hinzufügen: Fügen Sie
androidx.compose.material3.adaptive:adaptive-navigation3in die Dateibuild.gradle.ktsIhres Projekts ein. - Einträge mit
ListDetailSceneStrategy-Metadaten definieren: Verwenden SielistPane(), detailPane()undextraPane(), um IhreNavEntrysfür die entsprechende Bereichsdarstellung zu kennzeichnen. Mit demlistPane()-Helfer können Sie auch einendetailPlaceholderangeben, wenn kein Element ausgewählt ist. rememberListDetailSceneStrategy() verwenden: Diese zusammensetzbare Funktion stellt eine vorkonfigurierteListDetailSceneStrategybereit, die von einemNavDisplayverwendet werden kann.
Das folgende Snippet ist ein Beispiel für Activity, das die Verwendung von ListDetailSceneStrategy demonstriert:
@Serializable object ProductList : NavKey @Serializable data class ProductDetail(val id: String) : NavKey @Serializable data object Profile : NavKey class MaterialListDetailActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3AdaptiveApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Scaffold { paddingValues -> val backStack = rememberNavBackStack(ProductList) val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>() NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { backStack.removeLastOrNull() }, sceneStrategy = listDetailStrategy, entryProvider = entryProvider { entry<ProductList>( metadata = ListDetailSceneStrategy.listPane( detailPlaceholder = { ContentYellow("Choose a product from the list") } ) ) { ContentRed("Welcome to Nav3") { Button(onClick = { backStack.add(ProductDetail("ABC")) }) { Text("View product") } } } entry<ProductDetail>( metadata = ListDetailSceneStrategy.detailPane() ) { product -> ContentBlue("Product ${product.id} ", Modifier.background(PastelBlue)) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Button(onClick = { backStack.add(Profile) }) { Text("View profile") } } } } entry<Profile>( metadata = ListDetailSceneStrategy.extraPane() ) { ContentGreen("Profile") } } ) } } } }