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-Fenster-Erlebnisse nahtlos verwalten.
Szenen verstehen
In Navigation 3 ist ein Scene die grundlegende Einheit, die eine oder mehrere NavEntry-Instanzen rendert. Ein Scene ist ein bestimmter visueller Zustand oder Abschnitt der Benutzeroberfläche, der die Anzeige von Inhalten aus dem Back Stack enthalten und verwalten kann.
Jede Scene-Instanz wird eindeutig durch ihre key und die Klasse der Scene selbst identifiziert. Diese eindeutige Kennung ist wichtig, da sie die Animation auf oberster Ebene steuert, wenn sich die Scene ändert.
Die Schnittstelle Scene hat die folgenden Eigenschaften:
key: Any: Eine eindeutige Kennung für diese spezifischeScene-Instanz. Dieser Schlüssel sorgt in Kombination mit der Klasse desScenefür Eindeutigkeit, hauptsächlich für Animationszwecke.entries: List<NavEntry<T>>: Dies ist eine Liste vonNavEntry-Objekten, für deren Anzeige derSceneverantwortlich ist. Wichtig: Wenn dieselbeNavEntrywährend eines Übergangs (z.B. bei einem Übergang mit gemeinsamem Element) in mehrerenScenesangezeigt wird, wird ihr Inhalt nur vom neuesten Ziel-Scenegerendert, das sie anzeigt.previousEntries: List<NavEntry<T>>: Diese Property definiert dieNavEntrys, die sich ergeben, wenn eine „Zurück“-Aktion vom aktuellenSceneausgeführt wird. Sie ist für die Berechnung des richtigen Status der intelligenten „Zurück“-Geste unerlässlich, damit dieNavDisplayden richtigen vorherigen Status antizipieren und dorthin wechseln kann. Dies kann eine Szene mit einer anderen Klasse, einem anderen Schlüssel oder beidem sein.content: @Composable () -> Unit: Dies ist die zusammensetzbare Funktion, mit der Sie definieren, wie dieSceneihreentriesund alle umgebenden UI-Elemente rendert, die für dieseScenespezifisch sind.metadata: Map<String, Any>: Stellt anderen Bibliothekskomponenten wieNavDisplayszenenspezifische Informationen bereit. Standardmäßig wird dermetadatades letztenNavEntryinentrieszurückgegeben.
„equals“ und „hashCode“ in benutzerdefinierten Szenen implementieren
Benutzerdefinierte Scene-Implementierungen müssen equals und hashCode korrekt implementieren, um Folgendes zu unterstützen:
- Szenenstatusübergänge: Jedes Mal, wenn eine Szene anhand der Liste der Szenenstrategien berechnet wird, prüft
NavDisplay, ob die neue Szene mit der vorherigen übereinstimmt. Wenn eine Änderung erkannt wird, wird dieSeekableTransitionaktualisiert, die für den Übergang zwischen Szenen verwendet wird. Dies wirkt sich wiederum auf den Lebenszyklusstatus der Szenen aus. - Overlay-Verwaltung: Für
OverlayScenes (z. B. Dialogfelder) verwendetNavDisplaydas Szenenobjekt selbst alskey, um Overlay-Lebenszyklen und Exit-Animationen zu verfolgen.
Prüfen Sie, ob alle Properties, die den Inhalt der Szene definieren, z. B. key, entries und previousEntries, in den Implementierungen von equals und hashCode enthalten sind. Im Allgemeinen sollten Sie Callbacks (wie onBack) in der Implementierung dieser Methoden vermeiden, da sie Instanzen ändern können, ohne den logischen Szenenstatus zu ändern.
Szenenstrategien verstehen
Ein SceneStrategy ist der Mechanismus, der bestimmt, wie eine bestimmte Liste von NavEntrys aus dem Back Stack angeordnet und in ein Scene umgewandelt werden soll. Wenn ein SceneStrategy die aktuellen Back-Stack-Einträge erhält, stellt er sich im Wesentlichen zwei wichtige Fragen:
- Kann ich aus diesen Einträgen eine
Sceneerstellen? Wenn derSceneStrategyfeststellt, dass er die angegebenenNavEntrys verarbeiten und eine sinnvolleScenebilden kann (z.B. ein Dialogfeld oder ein Layout mit mehreren Bereichen), fährt er fort. Andernfalls wirdnullzurückgegeben, sodass andere Strategien die Möglichkeit haben, eineScenezu erstellen. - Wenn ja, wie sollte ich diese Einträge in der
Scene?anordnen? Sobald sich einSceneStrategyzur Verarbeitung der Einträge verpflichtet, übernimmt er die Verantwortung für die Erstellung einerSceneund die Definition, wie die angegebenenNavEntrys in dieserSceneangezeigt werden.
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 einen SceneStrategyScope, die den aktuellen List<NavEntry<T>> aus dem Back Stack übernimmt. Sie sollte einen Scene<T> zurückgeben, wenn sie erfolgreich einen aus den bereitgestellten Einträgen erstellen kann, oder null, wenn dies nicht möglich ist.
Der SceneStrategyScope ist für die Verwaltung aller optionalen Argumente verantwortlich, die der SceneStrategy möglicherweise benötigt, z. B. einen onBack-Callback.
Zusammenwirken von Szenen und Szenenstrategien
NavDisplay ist die zentrale komponierbare Funktion, die den Back Stack beobachtet und eine oder mehrere SceneStrategys verwendet, um die entsprechende Scene zu bestimmen und zu rendern.
Der Parameter sceneStrategies von NavDisplay erwartet eine Liste von SceneStrategy-Instanzen, die für die Berechnung der anzuzeigenden Scene verantwortlich sind. Wenn von den bereitgestellten Strategien kein Scene berechnet wird, greift NavDisplay automatisch auf die Verwendung eines SinglePaneSceneStrategy zurück.
Hier ist eine Aufschlüsselung der Interaktion:
- Wenn du Schlüssel aus dem Back Stack hinzufügst oder entfernst (z.B. mit
backStack.add()oderbackStack.removeLastOrNull()), werden diese Änderungen vomNavDisplaybeobachtet. - Der
NavDisplayübergibt die aktuelle Liste derNavEntrys (abgeleitet von den Back-Stack-Schlüsseln) der Reihe nach an die konfiguriertensceneStrategiess und ruft für jedencalculateSceneauf, bis einScenezurückgegeben wird. - Wenn ein
SceneStrategyerfolgreich einScenezurückgibt, rendert dieNavDisplaydann diecontentdiesesScene. DerNavDisplayverwaltet auch Animationen und die intelligente „Zurück“-Geste basierend auf den Attributen desScene.
Beispiel: Layout mit einem Bereich (Standardverhalten)
Das einfachste benutzerdefinierte Layout ist eine Anzeige mit einem einzelnen Bereich. Das ist das Standardverhalten, wenn kein anderes 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 Listen-Detailansicht-Layout (benutzerdefinierte Szene und Strategie)
In diesem Beispiel wird gezeigt, wie Sie ein Listen-Detail-Layout erstellen, das unter zwei Bedingungen aktiviert wird:
- Die Fensterbreite ist ausreichend, um zwei Bereiche zu unterstützen (mindestens
WIDTH_DP_MEDIUM_LOWER_BOUND). - Der Back Stack enthält Einträge, die über bestimmte Metadaten angegeben haben, dass sie in einem Listen-Detail-Layout angezeigt werden können.
Das folgende Snippet ist der Quellcode für ListDetailScene.kt und enthält sowohl ListDetailScene als auch ListDetailSceneStrategy:
// --- ListDetailScene --- /** * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split. * */ data class ListDetailScene<T : Any>( override val key: Any, override val previousEntries: List<NavEntry<T>>, val listEntry: NavEntry<T>, val detailEntry: NavEntry<T>, ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(listEntry, detailEntry) override val content: @Composable (() -> Unit) = { Row(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.weight(0.4f)) { listEntry.Content() } Column(modifier = Modifier.weight(0.6f)) { detailEntry.Content() } } } } @Composable fun <T : Any> rememberListDetailSceneStrategy(): ListDetailSceneStrategy<T> { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass return remember(windowSizeClass) { ListDetailSceneStrategy(windowSizeClass) } } // --- ListDetailSceneStrategy --- /** * A [SceneStrategy] that returns a [ListDetailScene] if the window is wide enough, the last item * is the backstack is a detail, and before it, at any point in the backstack is a list. */ class ListDetailSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> { override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? { if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { return null } val detailEntry = entries.lastOrNull()?.takeIf { it.metadata.contains(DetailKey) } ?: return null val listEntry = entries.findLast { it.metadata.contains(ListKey) } ?: return null // We use the list's contentKey to uniquely identify the scene. // This allows the detail panes to be displayed instantly through recomposition, rather than // having NavDisplay animate the whole scene out when the selected detail item changes. val sceneKey = listEntry.contentKey return ListDetailScene( key = sceneKey, previousEntries = entries.dropLast(1), listEntry = listEntry, detailEntry = detailEntry ) } object ListKey : NavMetadataKey<Boolean> object DetailKey : NavMetadataKey<Boolean> companion object { /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun listPane() = metadata { put(ListKey, true) } /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun detailPane() = metadata { put(DetailKey, true) } } }
Wenn Sie dieses ListDetailSceneStrategy in Ihrem NavDisplay verwenden möchten, ändern Sie Ihre entryProvider-Aufrufe so, dass sie ListDetailScene.listPane()-Metadaten für den Eintrag enthalten, den Sie als Listenlayout anzeigen möchten, und ListDetailScene.detailPane() für den Eintrag, den Sie als Detaillayout anzeigen möchten. Geben Sie dann ListDetailSceneStrategy() als sceneStrategy an und verwenden Sie den Standard-Fallback für Szenarien mit einem einzelnen Bereich:
// Define your navigation keys @Serializable data object ConversationList : NavKey @Serializable data class ConversationDetail(val id: String) : NavKey @Composable fun MyAppContent() { val backStack = rememberNavBackStack(ConversationList) val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>() NavDisplay( backStack = backStack, onBack = { backStack.removeLastOrNull() }, sceneStrategies = listOf(listDetailStrategy), entryProvider = entryProvider { entry<ConversationList>( metadata = ListDetailSceneStrategy.listPane() ) { Column(modifier = Modifier.fillMaxSize()) { Text(text = "I'm a Conversation List") Button(onClick = { backStack.addDetail(ConversationDetail("123")) }) { Text(text = "Open detail") } } } entry<ConversationDetail>( metadata = ListDetailSceneStrategy.detailPane() ) { Text(text = "I'm a Conversation Detail") } } ) } private fun NavBackStack<NavKey>.addDetail(detailRoute: ConversationDetail) { // Remove any existing detail routes, then add the new detail route removeIf { it is ConversationDetail } add(detailRoute) }
Wenn Sie keine eigene Listen-Detailansicht erstellen möchten, können Sie die Material-Listen-Detailansicht verwenden, die sinnvolle Details und Unterstützung für Platzhalter bietet, wie im nächsten Abschnitt gezeigt.
Listen-Detailansicht-Inhalte in einer adaptiven Material-Szene anzeigen
Für den Anwendungsfall „Listen-Detailansicht“ stellt das Artefakt androidx.compose.material3.adaptive:adaptive-navigation3 eine ListDetailSceneStrategy bereit, die eine Scene für die Listen-Detailansicht erstellt. Diese Scene verarbeitet automatisch komplexe Anordnungen mit mehreren Bereichen (Listen-, Detail- und zusätzliche Bereiche) und passt sie an die Fenstergröße und den Gerätestatus an.
So erstellen Sie eine Material-Listen-Detailansicht Scene:
- Abhängigkeit hinzufügen: Fügen Sie
androidx.compose.material3.adaptive:adaptive-navigation3in diebuild.gradle.kts-Datei Ihres Projekts ein. - Einträge mit
ListDetailSceneStrategy-Metadaten definieren: Verwenden SielistPane(), detailPane()undextraPane(), um IhreNavEntrysfür die entsprechende Bereichsanzeige zu markieren. Mit demlistPane()-Hilfsprogramm können Sie auch einendetailPlaceholderangeben, wenn kein Element ausgewählt ist. rememberListDetailSceneStrategy() verwenden: Diese zusammensetzbare Funktion bietet eine vorkonfigurierteListDetailSceneStrategy, die von einerNavDisplayverwendet werden kann.
Das folgende Snippet ist ein Beispiel für Activity, das die Verwendung von ListDetailSceneStrategy zeigt:
@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() }, sceneStrategies = listOf(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") } } ) } } } }