Mit Navigation 3 wird ein leistungsstarkes und flexibles System zum Verwalten des UI-Ablaufs Ihrer App über Szenen eingeführt. Mit Szenen können Sie hochgradig angepasste Layouts erstellen, sich an unterschiedliche Bildschirmgrößen anpassen und komplexe Mehrfenster-Erlebnisse nahtlos verwalten.
Szenen
In Navigation 3 ist eine Scene die Grundeinheit, die eine oder mehrere
NavEntry-Instanzen rendert. Stellen Sie sich eine Scene als einen bestimmten visuellen Zustand oder Abschnitt Ihrer UI vor, der die Anzeige von Inhalten aus Ihrem Back-Stack enthalten und verwalten kann.
Jede Scene Instanz wird eindeutig durch ihren key und die Klasse von
der Scene selbst identifiziert. Diese eindeutige Kennung ist wichtig, da sie die
Animation auf oberster Ebene steuert, wenn sich die Scene ändert.
Die Scene-Schnittstelle hat die folgenden Eigenschaften:
key: Any: Eine eindeutige Kennung für diese bestimmteScene-Instanz. Dieser Schlüssel sorgt in Kombination mit der Klasse derScenefür Eindeutigkeit, hauptsächlich für Animationszwecke.entries: List<NavEntry<T>>: Dies ist eine Liste vonNavEntry-Objekten, für deren AnzeigeSceneverantwortlich ist. Wichtig: Wenn dasselbeNavEntrywährend einer Übergangsanimation in mehrerenScenesangezeigt wird (z.B. bei einer Übergangsanimation für ein gemeinsames Element), werden seine Inhalte nur von der letzten Ziel-Scenegerendert, in der er angezeigt wird.previousEntries: List<NavEntry<T>>: Diese Eigenschaft definiert dieNavEntrys, die sich ergeben würden, wenn in der aktuellenSceneeine „Zurück“-Aktion ausgeführt wird. Sie ist wichtig, um den richtigen Status der intelligenten „Zurück“-Geste zu berechnen, damitNavDisplayden richtigen vorherigen Status vorhersagen und zu ihm übergehen kann. Das kann eine Szene mit einer anderen Klasse und/oder einem anderen Schlüssel 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 zur Verfügung. Standardmäßig wird diemetadatades letztenNavEntryinentrieszurückgegeben.
Szenenstrategien
Ein SceneStrategy ist der Mechanismus, der bestimmt, wie eine bestimmte Liste von
NavEntrys aus dem Back-Stack angeordnet und in eine
Scene überführt werden soll. Wenn eine SceneStrategy die aktuellen Back-Stack-Einträge erhält, stellt sie sich im Wesentlichen zwei Fragen:
- Kann ich aus diesen Einträgen eine
Sceneerstellen? Wenn dieSceneStrategyfeststellt, dass sie die angegebenenNavEntrys verarbeiten und eine sinnvolleSceneerstellen kann (z.B. ein Dialogfeld oder ein Layout mit mehreren Bereichen), wird fortgefahren. Andernfalls wirdnullzurückgegeben, sodass andere Strategien die Möglichkeit haben, eineScenezu erstellen. - Wenn ja, wie soll ich diese Einträge in der
Scene?anordnen? Sobald eineSceneStrategydie Einträge verarbeitet, ist sie für die Erstellung einerSceneund die Definition der Anzeige der angegebenenNavEntrys in dieserSceneverantwortlich.
Das Herzstück einer 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 die
aktuelle List<NavEntry<T>> aus dem Back-Stack verwendet. Sie sollte eine Scene<T>
zurückgeben, wenn sie aus den bereitgestellten Einträgen erfolgreich eine erstellen kann, oder null wenn das nicht
möglich ist.
Der SceneStrategyScope ist für die Verwaltung aller optionalen Argumente
verantwortlich, die die SceneStrategy möglicherweise benötigt, z. B. ein onBack-Callback.
Zusammenwirken von Szenen und Szenenstrategien
Die NavDisplay ist die zentrale zusammensetzbare Funktion, die Ihren Back-Stack beobachtet und
eine oder mehrere SceneStrategys verwendet, um die entsprechende
Scene zu bestimmen und zu rendern.
Der NavDisplay's sceneStrategies Parameter erwartet eine Liste von SceneStrategy
Instanzen, die für die Berechnung der anzuzeigenden Scene verantwortlich sind. Wenn von den bereitgestellten Strategien keine
Scene berechnet wird, greift NavDisplay automatisch
standardmäßig auf eine SinglePaneSceneStrategy zurück.
Hier ist eine Aufschlüsselung der Interaktion:
- Wenn Sie Ihrem Back-Stack Schlüssel hinzufügen oder daraus entfernen (z.B. mit
backStack.add()oderbackStack.removeLastOrNull()), werden diese Änderungen vonNavDisplaybeobachtet. - Der
NavDisplayübergibt die aktuelle Liste derNavEntrys (abgeleitet von den Back Stack-Schlüsseln) in der Reihenfolge an die konfiguriertensceneStrategiesund ruft für jedecalculateSceneauf, bis eineScenezurückgegeben wird. - Wenn ein
SceneStrategyerfolgreich einScenezurückgibt, rendertNavDisplaydann dencontentdiesesScene.NavDisplayverwaltet auch Animationen und die intelligente „Zurück“-Geste basierend auf den Eigenschaften derScene.
Beispiel: Layout mit einem Bereich (Standardverhalten)
Das einfachste benutzerdefinierte Layout ist eine Anzeige mit einem Bereich. Das 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 Listen-Detailansicht-Layout (benutzerdefinierte Scene und Strategie)
In diesem Beispiel wird gezeigt, wie Sie ein einfaches Listen-Detailansicht-Layout erstellen, das unter zwei Bedingungen aktiviert wird:
- Die Fensterbreite ist ausreichend, um zwei Bereiche zu unterstützen (d.h. mindestens
WIDTH_DP_MEDIUM_LOWER_BOUND). - Der Back-Stack enthält Einträge, die mit bestimmten Metadaten ihre Unterstützung für die Anzeige in einem Listen-Detailansicht-Layout deklariert haben.
Das folgende Snippet ist der Quellcode für ListDetailScene.kt und es
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. * */ 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 diese 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 verlassen Sie sich auf den Standard-Fallback für Szenarien mit einem 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.
Inhalte mit Listen-Detailansicht in einer adaptiven Material-Scene anzeigen
Für den Anwendungsfall mit Listen-Detailansicht bietet das
androidx.compose.material3.adaptive:adaptive-navigation3 Artefakt eine
ListDetailSceneStrategy, die eine Scene mit Listen-Detailansicht erstellt. Diese Scene verarbeitet automatisch komplexe Anordnungen mit mehreren Bereichen (Liste, Details 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 die Dateibuild.gradle.ktsIhres Projekts ein. - Einträge mit
ListDetailSceneStrategyMetadaten definieren: Verwenden SielistPane(), detailPane(), undextraPane(), um IhreNavEntrysfür die entsprechende Bereichsanzeige zu kennzeichnen. Mit dem HelferlistPane()können Sie auch einendetailPlaceholderangeben, wenn kein Element ausgewählt ist. - Verwenden Sie
rememberListDetailSceneStrategy(): Diese zusammensetzbare Funktion bietet eine vorkonfigurierteListDetailSceneStrategy, die von einemNavDisplayverwendet werden kann.
Das folgende Snippet ist ein Beispiel für eine Activity, in der die Verwendung von ListDetailSceneStrategy veranschaulicht wird:
@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") } } ) } } } }