Nawigacja 3 wprowadza zaawansowany i elastyczny system zarządzania przepływem interfejsu użytkownika aplikacji za pomocą scen. Sceny umożliwiają tworzenie bardzo dopracowanych układów, dostosowywanie ich do różnych rozmiarów ekranu i płynne zarządzanie złożonymi interfejsami z wieloma panelami.
Sceny
W Nawigacji 3 element Scene
jest podstawową jednostką, która renderuje co najmniej 1 element NavEntry
. Scene
to odrębny stan wizualny lub sekcja interfejsu użytkownika, która może zawierać treści z back-endu i zarządzać ich wyświetlaniem.
Każda instancja Scene
jest jednoznacznie identyfikowana przez swój key
oraz klasę Scene
. Ten unikalny identyfikator jest kluczowy, ponieważ powoduje animację najwyższego poziomu po zmianie wartości Scene
.
Interfejs Scene
ma te właściwości:
key: Any
: unikalny identyfikator tej konkretnej instancjiScene
. Ten klucz w połączeniu z klasąScene
zapewnia odrębność, głównie na potrzeby animacji.entries: List<NavEntry<T>>
: lista obiektówNavEntry
, za wyświetlanie których odpowiada elementScene
. Podczas przejścia (np.w przypadku przejścia elementu współdzielonego) ta sama wartośćNavEntry
może być wyświetlana w kilku elementachScenes
. W takim przypadku jej zawartość zostanie wyrenderowana tylko przez ostatni element docelowyScene
, który ją wyświetla.previousEntries: List<NavEntry<T>>
: ta właściwość definiuje wartościNavEntry
, które wystąpią, jeśli z bieżącegoScene
nastąpi działanie „wstecz”. Jest to niezbędne do obliczenia prawidłowego stanu wstecznego przewidywanego, co pozwalaNavDisplay
przewidzieć i przejść do prawidłowego poprzedniego stanu, który może być sceną z inną klasą lub innym kluczem.content: @Composable () -> Unit
: to funkcja składana, w której definiujesz sposób renderowania elementuentries
oraz wszystkich elementów interfejsu otaczających ten element.Scene
Scene
Strategie dotyczące scen
SceneStrategy
to mechanizm, który określa, jak dana lista NavEntry
ze stosu z poziomu podrzędnego powinna być uporządkowana i przekształcona w Scene
. Gdy SceneStrategy
zobaczy bieżące wpisy w steku wywołania, zada sobie 2 kluczowe pytania:
- Czy mogę utworzyć
Scene
na podstawie tych wpisów? JeśliSceneStrategy
zauważy, że może obsłużyć daneNavEntry
i utworzyć sensowneScene
(np. dialog lub układ z wieloma panelami), kontynuuje. W przeciwnym razie zwraca wartośćnull
, co daje szansę innym strategiom na utworzenieScene
. - Jeśli tak, jak te wpisy należy uporządkować w
Scene?
GdySceneStrategy
zdecyduje się na obsługę wpisów, przejmuje odpowiedzialność za tworzenieScene
i określanie sposobu wyświetlania określonychNavEntry
w tymScene
.
Istotą SceneStrategy
jest metoda calculateScene
:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
Ta metoda przyjmuje bieżącą wartość List<NavEntry<T>>
ze stosu i wywołanie zwrotne onBack
. Powinien zwrócić wartość Scene<T>
, jeśli uda się utworzyć obiekt na podstawie podanych wpisów, lub null
, jeśli nie uda się tego zrobić.
SceneStrategy
udostępnia też wygodną funkcję then
, która pozwala łączyć ze sobą wiele strategii. Dzięki temu powstaje elastyczny kanał podejmowania decyzji, w którym każda strategia może próbować obliczyć wartość Scene
. Jeśli nie uda się to, przekazuje zadanie do wykonania kolejnej strategii w łańcuchu.
Współdziałanie scen i strategii scen
NavDisplay
to główny komponent, który obserwuje twój stos z poziomu warstwy podstawowej i korzysta z elementu SceneStrategy
, aby określić i wyrenderować odpowiedni element Scene
.
Parametr NavDisplay's sceneStrategy
oczekuje wartości SceneStrategy
, która odpowiada za obliczenie wyświetlanej wartości Scene
. Jeśli strategia (lub łańcuch strategii) nie oblicza wartości Scene
, NavDisplay
automatycznie przechodzi do domyślnego korzystania z SinglePaneSceneStrategy
.
Oto zestawienie interakcji:
- Gdy dodasz lub usuniesz klucze ze stosu (np. za pomocą funkcji
backStack.add()
lubbackStack.removeLastOrNull()
), funkcjaNavDisplay
zauważy te zmiany. - Funkcja
NavDisplay
przekazuje bieżącą listęNavEntrys
(wywoływaną z kluczy w backstacku) do skonfigurowanej metodySceneStrategy's calculateScene
. - Jeśli
SceneStrategy
zwróciScene
,NavDisplay
wyrenderujecontent
tegoScene
.NavDisplay
zarządza też animacjami i wsteczną predykcją na podstawie właściwościScene
.
Przykład: układ z jednym panelem (domyślne działanie)
Najprostszy niestandardowy układ to wyświetlanie w jednej karcie, co jest domyślnym zachowaniem, jeśli żaden inny SceneStrategy
nie ma pierwszeństwa.
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) ) }
Przykład: podstawowy układ z 2 panelami (niestandardowa scena i strategia)
Ten przykład pokazuje, jak utworzyć proste rozmieszczenie na 2 panelach, które jest aktywowane na podstawie 2 warunków:
- Szerokość okna jest wystarczająca, aby wyświetlić 2 panele (czyli co najmniej
WIDTH_DP_MEDIUM_LOWER_BOUND
). - 2 najwyższe pozycje w grupie elementów na końcu deklarują obsługę wyświetlania w układzie z 2 panelami za pomocą określonych metadanych.
Poniższy fragment kodu to połączony kod źródłowy funkcji TwoPaneScene.kt
i 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 } } }
Aby używać TwoPaneSceneStrategy
w Twoim NavDisplay
, zmodyfikuj wywołania entryProvider
, aby uwzględnić metadane TwoPaneScene.twoPane()
dotyczące wpisów, które chcesz wyświetlać w układzie z 2 panelami. Następnie podaj wartość TwoPaneSceneStrategy()
jako sceneStrategy
, korzystając z domyślnego rozwiązania zastępczego w scenariuszach jednopanelowych:
// 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() } } } ) }
Wyświetlanie treści z listy w ramach sceny adaptacyjnej z użyciem materiału
W przypadku przypadku użycia listy z szczegółami artefakt androidx.compose.material3.adaptive:adaptive-navigation3
udostępnia element ListDetailSceneStrategy
, który tworzy listę z szczegółami Scene
. Scene
automatycznie obsługuje złożone układy z wieloma panelami (listy, panele szczegółowe i dodatkowe) oraz dostosowuje je na podstawie rozmiaru okna i stanu urządzenia.
Aby utworzyć listę szczegółów komponentu Scene
, wykonaj te czynności:
- Dodaj zależność: dodaj plik
androidx.compose.material3.adaptive:adaptive-navigation3
do plikubuild.gradle.kts
projektu. - Definiowanie wpisów za pomocą metadanych
ListDetailSceneStrategy
: użyj tagówlistPane(), detailPane()
iextraPane()
, aby oznaczyćNavEntrys
na potrzeby wyświetlania w odpowiedniej karcie. PomocniklistPane()
umożliwia też określenie wartościdetailPlaceholder
, gdy nie wybrano żadnego elementu. - Użyj funkcji
rememberListDetailSceneStrategy
(): ta funkcja składana udostępnia wstępnie skonfigurowany elementListDetailSceneStrategy
, którego może używać elementNavDisplay
.
Ten fragment kodu to przykładowa funkcja Activity
, która demonstruje użycie funkcji ListDetailSceneStrategy
:
@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") } } ) } } } }