Nawigacja 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 się 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 wyświetlanie których odpowiadaScene. Ważne jest to, że jeśli ten samNavEntryjest wyświetlany w wieluScenespodczas przejścia (np. w przejściu wspólnego 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, jakScenerenderuje swój elemententriesi wszystkie otaczające elementy interfejsu użytkownika specyficzne dla tego elementuScene.metadata: Map<String, Any>: przekazuje informacje o scenie do innych komponentów biblioteki, np.NavDisplay. Domyślnie zwracametadataostatniegoNavEntrywentries.
Informacje o strategiach dotyczących 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. Zasadniczo, gdy aplikacja SceneStrategy otrzymuje bieżące wpisy na stosie wstecznym, 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 jest to możliwe.
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 centralna funkcja kompozycyjna, która obserwuje stos wsteczny i za pomocą SceneStrategy określa oraz renderuje odpowiedni Scene.
Parametr NavDisplay's sceneStrategy oczekuje SceneStrategy, który jest odpowiedzialny za obliczanie Scene do wyświetlenia. Jeśli podana strategia (lub łańcuch strategii) nie obliczy wartości Scene, 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ęNavEntrys(pochodną kluczy stosu wstecznego) do skonfigurowanej metodySceneStrategy's calculateScene.- Jeśli
SceneStrategyzwróciScene,NavDisplaywyrenderujecontenttegoScene.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świetlanie w jednym panelu, które jest domyślnym działaniem, jeśli żadne inne SceneStrategy nie mają wyższego priorytetu.
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 wyświetlić 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 kodu 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, jak pokazano w następnej sekcji.
Wyświetlanie treści ze szczegółowej listy w scenie adaptacyjnej Material Design
W przypadku użycia szczegółowej listy artefakt androidx.compose.material3.adaptive:adaptive-navigation3 udostępnia ListDetailSceneStrategy, który tworzy Scene szczegółową listę. Ten komponent 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ółów 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. FunkcjalistPane()umożliwia też określenie wartościdetailPlaceholder, gdy nie wybrano żadnego elementu. - Użyj
rememberListDetailSceneStrategy(): ta funkcja typu „composable” udostępnia wstępnie skonfigurowany elementListDetailSceneStrategy, którego może używać elementNavDisplay.
Poniższy fragment to przykładowy 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") } } ) } } } }