Biblioteka Navigation 3 wprowadza zaawansowany i elastyczny system zarządzania przepływem interfejsu aplikacji za pomocą scen. Sceny umożliwiają tworzenie wysoce spersonalizowanych układów, dostosowywanie ich do różnych rozmiarów ekranu i płynne zarządzanie złożonymi interfejsami wielopanelowymi.
Informacje o scenach
W Navigation 3 Scene to podstawowa jednostka, która renderuje co najmniej 1 instancję NavEntry. Scene to odrębny stan wizualny lub sekcja interfejsu, która może zawierać i zarządzać wyświetlaniem treści z listy wstecznej.
Każde wystąpienie Scene jest jednoznacznie identyfikowane przez jego key i klasę samego Scene. Ten unikalny identyfikator jest kluczowy, ponieważ odpowiada za animację najwyższego poziomu, gdy zmienia się Scene.
Interfejs Scene ma te właściwości:
key: Any: unikalny identyfikator tej konkretnej instancjiScene. Ten klucz w połączeniu z klasąScenezapewnia odrębność, głównie na potrzeby animacji.entries: List<NavEntry<T>>: Jest to lista obiektówNavEntry, za których wyświetlanie odpowiadaScene. Ważne jest to, że jeśli ten samNavEntryjest wyświetlany w wieluScenespodczas przejścia (np. w przejściu udostępnionego elementu), jego treść będzie renderowana tylko przez najnowszy docelowyScene, który go wyświetla.previousEntries: List<NavEntry<T>>: ta właściwość określaNavEntry, które pojawią się, gdy z bieżącegoScenezostanie wykonana czynność „wstecz”. Jest to niezbędne do obliczenia prawidłowego stanu przewidywanego przejścia wstecz, co pozwalaNavDisplayprzewidywać i przechodzić do prawidłowego poprzedniego stanu, który może być sceną o innej klasie lub kluczu.content: @Composable () -> Unit: jest to funkcja typu „composable”, w której określasz, jak komponentScenerenderuje swój elemententriesi wszystkie otaczające go elementy interfejsu użytkownika, które są specyficzne dla tego komponentuScene.metadata: Map<String, Any>: udostępnia informacje o scenie innym komponentom biblioteki, takim jakNavDisplay. Domyślnie zwracametadataostatniegoNavEntryw zakresieentries.
Informacje o strategiach scen
SceneStrategy to mechanizm, który określa, jak dana lista NavEntry z listy wstecznej powinna być ułożona i przekształcona w Scene. Gdy aplikacja SceneStrategy otrzymuje bieżące wpisy na liście wstecznej, zadaje sobie 2 kluczowe pytania:
- Czy na podstawie tych wpisów mogę utworzyć
Scene? JeśliSceneStrategystwierdzi, że może obsłużyć podaneNavEntryi utworzyć sensownyScene(np. okno dialogowe lub układ wielopanelowy), przechodzi dalej. W przeciwnym razie zwraca wartośćnull, dając innym strategiom szansę na utworzenie wartościScene. - Jeśli tak, jak mam uporządkować te wpisy w
Scene?GdySceneStrategyzobowiąże się do obsługi wpisów, przejmuje odpowiedzialność za utworzenieScenei określenie, jak określoneNavEntrybędą wyświetlane w tymScene.
Podstawą SceneStrategy jest metoda calculateScene:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
Ta metoda jest funkcją rozszerzającą w przypadku SceneStrategyScope, która pobiera bieżący List<NavEntry<T>> ze stosu wstecznego. Powinien zwracać wartość Scene<T>, jeśli na podstawie podanych wpisów można utworzyć listę, lub null, jeśli nie można.
SceneStrategyScope odpowiada za utrzymywanie wszelkich argumentów opcjonalnych, których może potrzebować SceneStrategy, np. wywołania zwrotnego onBack.
Współdziałanie scen i strategii scen
NavDisplay to centralny komponent, który obserwuje stos wsteczny i używa co najmniej 1 komponentu SceneStrategy do określania i renderowania odpowiedniego komponentu Scene.
Parametr NavDisplay's sceneStrategies oczekuje listy SceneStrategyinstancji, które są odpowiedzialne za obliczanie Scene do wyświetlenia. Jeśli żadna z podanych strategii nie obliczy wartości Scene, usługa NavDisplay automatycznie powróci do domyślnego używania wartości SinglePaneSceneStrategy.
Oto opis interakcji:
- Gdy dodasz lub usuniesz klucze z listy wstecznej (np. za pomocą
backStack.add()lubbackStack.removeLastOrNull()),NavDisplayzarejestruje te zmiany. NavDisplayprzekazuje bieżącą listęNavEntry(pochodzących z kluczy w stosie wstecznym) do skonfigurowanegosceneStrategiesw określonej kolejności, wywołująccalculateScenew przypadku każdego z nich, dopóki nie zostanie zwróconyScene.- Gdy
SceneStrategyzwróciScene,NavDisplayrenderujecontenttegoScene.NavDisplayzarządza też animacjami i przewidywanym przejściem wstecz na podstawie właściwościScene.
Przykład: układ z jednym panelem (domyślne działanie)
Najprostszy układ niestandardowy to wyświetlacz z 1 panelem, który jest domyślnym zachowaniem, jeśli nie ma innego SceneStrategy o wyższym priorytecie.
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) ) }
Przykład: podstawowy układ szczegółowej listy (niestandardowa scena i strategia)
Ten przykład pokazuje, jak utworzyć prosty układ szczegółowej listy, który jest aktywowany na podstawie 2 warunków:
- Szerokość okna jest wystarczająca, aby pomieścić 2 panele (czyli co najmniej
WIDTH_DP_MEDIUM_LOWER_BOUND). - Stos wsteczny zawiera wpisy, które zadeklarowały obsługę wyświetlania w układzie szczegółowej listy za pomocą określonych metadanych.
Poniższy fragment to kod źródłowy ListDetailScene.kt, który zawiera zarówno ListDetailScene, jak i 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) } } }
Aby użyć tego ListDetailSceneStrategy w NavDisplay, zmodyfikuj wywołania entryProvider, aby uwzględniały metadane ListDetailScene.listPane() dla wpisu, który chcesz wyświetlać w układzie listy, oraz ListDetailScene.detailPane() dla wpisu, który chcesz wyświetlać w układzie szczegółów. Następnie podaj ListDetailSceneStrategy() jako sceneStrategy, korzystając z domyślnego rozwiązania w przypadku scenariuszy z jednym panelem:
// 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) }
Jeśli nie chcesz tworzyć własnej sceny z listą i szczegółami, możesz użyć sceny z listą i szczegółami Material Design, która zawiera przydatne szczegóły i obsługuje symbole zastępcze, co pokazano w następnej sekcji.
Wyświetlanie treści szczegółowej listy w adaptacyjnej scenie Material
W przypadku użycia szczegółowej listy artefakt androidx.compose.material3.adaptive:adaptive-navigation3 udostępnia ListDetailSceneStrategy, który tworzy szczegółową listę Scene. Ta funkcja Sceneautomatycznie obsługuje złożone układy wielopanelowe (listy, szczegóły i dodatkowe panele) oraz dostosowuje je do rozmiaru okna i stanu urządzenia.
Aby utworzyć listę szczegółową Material Scene, wykonaj te czynności:
- Dodaj zależność: w pliku
build.gradle.ktsprojektu umieśćandroidx.compose.material3.adaptive:adaptive-navigation3. - Określaj wpisy za pomocą
ListDetailSceneStrategymetadanych: używaj tagówlistPane(), detailPane()iextraPane(), aby oznaczyćNavEntrysdo wyświetlania w odpowiednim panelu. PomocniklistPane()umożliwia też określeniedetailPlaceholder, gdy nie jest wybrany żaden element. - Użyj
rememberListDetailSceneStrategy(): ta funkcja typu „composable” udostępnia wstępnie skonfigurowany elementListDetailSceneStrategy, którego może używać elementNavDisplay.
Poniższy fragment kodu to przykład Activity, który pokazuje użycie 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<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") } } ) } } } }