Navigation 3 bietet ein leistungsstarkes und flexibles System zum Verwalten des UI-Flows Ihrer App über Szenen. Mit Szenen können Sie Layouts individuell anpassen, an unterschiedliche Bildschirmgrößen anpassen und komplexe Ansichten mit mehreren Ansichten nahtlos verwalten.
Szenen
In Navigation 3 ist eine Scene
die grundlegende Einheit, die eine oder mehrere NavEntry
-Instanzen rendert. Stellen Sie sich ein Scene
als einen bestimmten visuellen Zustand oder Bereich Ihrer Benutzeroberfläche vor, der Inhalte aus Ihrem Backstack enthalten und verwalten kann.
Jede Scene
-Instanz wird durch ihren key
und die Klasse der Scene
selbst eindeutig identifiziert. Diese eindeutige Kennung ist entscheidend, da sie die Animation der obersten Ebene steuert, wenn sich die Scene
ändert.
Die Scene
-Benutzeroberfläche hat die folgenden Eigenschaften:
key: Any
: Eine eindeutige Kennung für diese spezifischeScene
-Instanz. Dieser Schlüssel in Kombination mit der Klasse vonScene
sorgt für Unterscheidungsmerkmale, vor allem zu Animationszwecken.entries: List<NavEntry<T>>
: Liste derNavEntry
-Objekte, die vomScene
angezeigt werden sollen. Wenn dieselbeNavEntry
während eines Übergangs in mehrerenScenes
angezeigt wird (z.B. in einem Übergang mit freigegebenen Elementen), wird ihr Inhalt nur vom jeweils letzten ZielScene
gerendert, in dem sie angezeigt wird.previousEntries: List<NavEntry<T>>
: Mit dieser Property werden dieNavEntry
s definiert, die ausgeführt werden, wenn von der aktuellenScene
aus die Aktion „Zurück“ ausgeführt wird. Sie ist für die Berechnung des korrekten proaktiven Rückstatus unerlässlich, sodassNavDisplay
den korrekten vorherigen Status vorhersagen und darauf umstellen kann. Dies 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 festlegen, wie dieScene
ihreentries
und alle zugehörigen UI-Elemente für dieseScene
rendert.
Szenenstrategien
Ein SceneStrategy
ist der Mechanismus, der bestimmt, wie eine bestimmte Liste von NavEntry
s aus dem Backstack angeordnet und in eine Scene
übergeben werden soll. Wenn SceneStrategy
die aktuellen Backstack-Einträge sieht, stellt er sich im Grunde zwei wichtige Fragen:
- Kann ich aus diesen Einträgen eine
Scene
erstellen? Wenn derSceneStrategy
feststellt, dass er die angegebenenNavEntry
verarbeiten und eine sinnvolleScene
bilden kann (z.B. ein Dialogfeld oder ein mehrspaltiges Layout), fährt er fort. Andernfalls wirdnull
zurückgegeben, sodass andere Strategien die Möglichkeit haben, einenScene
zu erstellen. - Wenn ja, wie sollte ich diese Einträge in der
Scene?
anordnen? Sobald einSceneStrategy
die Verarbeitung der Einträge übernimmt, ist er dafür verantwortlich, eineScene
zu erstellen und zu definieren, wie die angegebenenNavEntry
s in dieserScene
angezeigt werden.
Der Kern eines SceneStrategy
ist die calculateScene
-Methode:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
Diese Methode nimmt die aktuelle List<NavEntry<T>>
aus dem Backstack und einen onBack
-Callback an. Es sollte eine Scene<T>
zurückgeben, wenn eine solche aus den angegebenen Einträgen gebildet werden kann, oder null
, wenn dies nicht möglich ist.
SceneStrategy
bietet auch eine praktische Infixfunktion then
, mit der Sie mehrere Strategien verketten können. So entsteht eine flexible Entscheidungspipeline, in der jede Strategie versuchen kann, Scene
zu berechnen. Wenn das nicht möglich ist, wird die Aufgabe an die nächste Strategie in der Kette weitergeleitet.
Zusammenspiel von Szenen und Szenenstrategien
Die NavDisplay
ist das zentrale Composeable, das Ihren Backstack beobachtet und mithilfe eines SceneStrategy
die entsprechende Scene
ermittelt und rendert.
Für den Parameter NavDisplay's sceneStrategy
ist ein SceneStrategy
erforderlich, der für die Berechnung der anzuzeigenden Scene
verantwortlich ist. Wenn mit der angegebenen Strategie (oder Strategiekette) kein Scene
berechnet wird, wird für NavDisplay
standardmäßig automatisch SinglePaneSceneStrategy
verwendet.
Hier eine Aufschlüsselung der Interaktion:
- Wenn Sie Ihrem Backstack Schlüssel hinzufügen oder daraus entfernen (z.B. mit
backStack.add()
oderbackStack.removeLastOrNull()
), werden diese Änderungen vonNavDisplay
berücksichtigt. - Die
NavDisplay
gibt die aktuelle Liste derNavEntrys
(abgeleitet aus den Backstack-Schlüsseln) an die konfigurierteSceneStrategy's calculateScene
-Methode weiter. - Wenn die
SceneStrategy
eineScene
zurückgibt, rendert dieNavDisplay
diecontent
dieserScene
. Außerdem verwaltetNavDisplay
Animationen und die Vorhersage der Rückgabe basierend auf den Eigenschaften vonScene
.
Beispiel: Layout mit einem einzigen Bereich (Standardverhalten)
Das einfachste benutzerdefinierte Layout ist ein einzeiliger Bildschirm. Das ist das Standardverhalten, wenn keine andere SceneStrategy
Vorrang hat.
data class SinglePaneScene<T : Any>( override val key: T, 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.invoke(entry.key) } } /** * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the * list. */ public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> { @Composable override fun calculateScene(entries: List<NavEntry<T>>, onBack: (Int) -> Unit): Scene<T> = SinglePaneScene( key = entries.last().key, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
Beispiel: Einfaches Layout mit zwei Ansichten (benutzerdefinierte Szene und Strategie)
In diesem Beispiel wird gezeigt, wie Sie ein einfaches Layout mit zwei Ansichten erstellen, das unter zwei Bedingungen aktiviert wird:
- Die Fensterbreite ist groß genug, um zwei Bereiche zu unterstützen (d.h. mindestens
WIDTH_DP_MEDIUM_LOWER_BOUND
). - Die ersten beiden Einträge im Backstack geben explizit an, dass sie mithilfe bestimmter Metadaten in einem Zwei-Spalten-Layout angezeigt werden können.
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.invoke(firstEntry.key) } Column(modifier = Modifier.weight(0.5f)) { secondEntry.content.invoke(secondEntry.key) } } } 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) } } // --- 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> : SceneStrategy<T> { @OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3WindowSizeClassApi::class) @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass // 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.key, secondEntry.key) 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 Ihrer NavDisplay
verwenden möchten, ändern Sie Ihre entryProvider
-Aufrufe so, dass sie TwoPaneScene.twoPane()
-Metadaten für die Einträge enthalten, die in einem zweispaltigen Layout angezeigt werden sollen. Geben Sie dann TwoPaneSceneStrategy()
als sceneStrategy
an und verwenden Sie den Standard-Fallback für Szenarien mit einem einzigen Steuerfeld:
// 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 = TwoPaneSceneStrategy<Any>(), onBack = { count -> repeat(count) { if (backStack.isNotEmpty()) { backStack.removeLastOrNull() } } } ) }
Listendetailinhalte in einer adaptiven Material-Szene anzeigen
Für den Anwendungsfall „Liste – Detail“ enthält das androidx.compose.material3.adaptive:adaptive-navigation3
-Artefakt eine ListDetailSceneStrategy
, mit der eine Scene
für „Liste – Detail“ erstellt wird. Diese Scene
verarbeitet automatisch komplexe mehrzeilige Anordnungen (Listen-, Detail- und Zusatzbereiche) und passt sie an die Fenstergröße und den Gerätestatus an.
So erstellen Sie ein Materiallistendetail Scene
:
- Fügen Sie die Abhängigkeit hinzu: Fügen Sie
androidx.compose.material3.adaptive:adaptive-navigation3
in die Dateibuild.gradle.kts
Ihres Projekts ein. - Einträge mit
ListDetailSceneStrategy
-Metadaten definieren: Verwenden SielistPane(), detailPane()
undextraPane()
, um IhreNavEntrys
für die entsprechende Ansicht im Bereich zu kennzeichnen. Mit demlistPane()
-Hilfselement können Sie auch einendetailPlaceholder
angeben, wenn kein Element ausgewählt ist. rememberListDetailSceneStrategy
() verwenden: Diese kombinierbare Funktion bietet eine vorkonfigurierteListDetailSceneStrategy
, die von einerNavDisplay
verwendet werden kann.
Das folgende Snippet ist ein Beispiel für Activity
, das die Verwendung von ListDetailSceneStrategy
veranschaulicht:
@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<Any>() NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { keysToRemove -> repeat(keysToRemove) { 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") } } ) } } } }