Navigation 3 introduce un sistema potente e flessibile per gestire il flusso dell'interfaccia utente della tua app tramite le Scene. Le scene ti consentono di creare layout altamente personalizzati, adattarsi a diverse dimensioni dello schermo e gestire senza problemi esperienze complesse con più riquadri.
Informazioni sulle scene
In Navigazione 3, un Scene è l'unità fondamentale che esegue il rendering di una o più istanze di
NavEntry. Pensa a un Scene come a uno stato visivo o una sezione distinta
della tua UI che può contenere e gestire la visualizzazione dei contenuti del tuo back
stack.
Ogni istanza di Scene è identificata in modo univoco dal relativo key e dalla classe di Scene stessa. Questo identificatore univoco è fondamentale perché determina l'animazione di primo livello quando cambia Scene.
L'interfaccia Scene ha le seguenti proprietà:
key: Any: un identificatore univoco per questa istanza specifica diScene. Questa chiave, combinata con la classe diScene, garantisce la distinzione, principalmente per scopi di animazione.entries: List<NavEntry<T>>: questo è un elenco di oggettiNavEntrycheSceneè responsabile della visualizzazione. È importante sottolineare che se lo stessoNavEntryviene visualizzato in piùScenesdurante una transizione (ad es. in una transizione di elementi condivisi), il suo contenuto verrà visualizzato solo dallaScenedi destinazione più recente.previousEntries: List<NavEntry<T>>: questa proprietà definisce gliNavEntryche si otterrebbero se si eseguisse un'azione "Indietro" dall'attualeScene. È essenziale per calcolare lo stato precedente predittivo corretto, consentendo all'NavDisplaydi prevedere e passare allo stato precedente corretto, che potrebbe essere una scena con una classe e/o una chiave diversa.content: @Composable () -> Unit: questa è la funzione componibile in cui definisci il rendering diScenedel relativoentriese di eventuali elementi UI circostanti specifici perScene.
Informazioni sulle strategie di scena
Un SceneStrategy è il meccanismo che determina come deve essere disposta e trasferita una determinata lista di
NavEntry dalla cronologia precedente in un
Scene. In sostanza, quando vengono presentate le voci correnti dello stack precedente, un
SceneStrategy si pone due domande chiave:
- Posso creare un
Sceneda queste voci? SeSceneStrategydetermina di poter gestire iNavEntryforniti e formare unScenesignificativo (ad es. una finestra di dialogo o un layout a più riquadri), procede. In caso contrario, restituiscenull, dando ad altre strategie la possibilità di creare unScene. - In caso affermativo, come devo disporre queste voci nel
Scene?Una volta che unSceneStrategysi impegna a gestire le voci, si assume la responsabilità di costruire unScenee definire come verranno visualizzati iNavEntryspecificati all'interno di questoScene.
Il cuore di un SceneStrategy è il suo metodo calculateScene:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
Questo metodo è una funzione di estensione di un SceneStrategyScope che prende l'List<NavEntry<T>> corrente dallo stack precedente. Dovrebbe restituire un Scene<T>
se riesce a formarne uno dalle voci fornite o null se
non riesce.
SceneStrategyScope è responsabile della gestione di eventuali argomenti facoltativi
di cui SceneStrategy potrebbe aver bisogno, ad esempio un callback onBack.
SceneStrategy fornisce anche una comoda funzione infissa then, che ti consente di concatenare più strategie. In questo modo viene creata una pipeline decisionale flessibile in cui ogni strategia può tentare di calcolare un Scene e, se non riesce, delega la strategia successiva nella catena.
Come funzionano insieme le scene e le strategie di scena
NavDisplay è il componente componibile centrale che osserva lo stack precedente e
utilizza un SceneStrategy per determinare e visualizzare il Scene appropriato.
Il parametro NavDisplay's sceneStrategy prevede un SceneStrategy che
si occupa di calcolare il Scene da visualizzare. Se non viene calcolato alcun Scene
dalla strategia (o dalla catena di strategie) fornita, NavDisplay torna automaticamente
all'utilizzo di un SinglePaneSceneStrategy per impostazione predefinita.
Ecco una suddivisione dell'interazione:
- Quando aggiungi o rimuovi chiavi dallo stack precedente (ad es. utilizzando
backStack.add()obackStack.removeLastOrNull()),NavDisplayosserva queste modifiche. NavDisplaypassa l'elenco attuale diNavEntrys(derivato dalle chiavi dello stack) al metodoSceneStrategy's calculateSceneconfigurato.- Se
SceneStrategyrestituisce correttamente unScene,NavDisplayesegue il rendering dicontentdiScene.NavDisplaygestisce anche le animazioni e il ritorno predittivo in base alle proprietà diScene.
Esempio: layout a un solo riquadro (comportamento predefinito)
Il layout personalizzato più semplice è la visualizzazione a un solo riquadro, che è il
comportamento predefinito se nessun altro SceneStrategy ha la precedenza.
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> { @Composable override fun calculateScene(entries: List<NavEntry<T>>, onBack: (Int) -> Unit): Scene<T> = SinglePaneScene( key = entries.last().contentKey, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
Esempio: layout di base a due riquadri (scena e strategia personalizzate)
Questo esempio mostra come creare un semplice layout a due riquadri che viene attivato in base a due condizioni:
- La larghezza della finestra è sufficientemente ampia da supportare due riquadri (ovvero almeno
WIDTH_DP_MEDIUM_LOWER_BOUND). - Le prime due voci dello stack secondario dichiarano esplicitamente il supporto per la visualizzazione in un layout a due riquadri utilizzando metadati specifici.
Il seguente snippet è il codice sorgente combinato per TwoPaneScene.kt e
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() } Column(modifier = Modifier.weight(0.5f)) { secondEntry.Content() } } } 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.contentKey, secondEntry.contentKey) 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 } } }
Per utilizzare questo TwoPaneSceneStrategy nel tuo NavDisplay, modifica le chiamate entryProvider in modo da includere i metadati TwoPaneScene.twoPane() per le voci che intendi mostrare in un layout a due riquadri. Poi, fornisci
TwoPaneSceneStrategy() come sceneStrategy, affidandoti al fallback
predefinito per gli scenari a un solo riquadro:
// 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() } } } ) }
Visualizzare i contenuti elenco-dettaglio in una scena adattiva Material
Per lo scenario di utilizzo elenco-dettagli, l'artefatto
androidx.compose.material3.adaptive:adaptive-navigation3 fornisce un
ListDetailSceneStrategy che crea un Scene elenco-dettagli. Questo Scene
gestisce automaticamente le complesse disposizioni a più riquadri (elenco, dettagli e riquadri
aggiuntivi) e le adatta in base alle dimensioni della finestra e allo stato del dispositivo.
Per creare un elenco dettagliato dei materiali Scene:
- Aggiungi la dipendenza: includi
androidx.compose.material3.adaptive:adaptive-navigation3nel filebuild.gradle.ktsdel progetto. - Definisci le voci con i metadati
ListDetailSceneStrategy: utilizzalistPane(), detailPane()eextraPane()per contrassegnareNavEntrysper la visualizzazione corretta del riquadro. L'helperlistPane()ti consente anche di specificare undetailPlaceholderquando non è selezionato alcun elemento. - Utilizza
rememberListDetailSceneStrategy(): questa funzione componibile fornisce unListDetailSceneStrategypreconfigurato che può essere utilizzato da unNavDisplay.
Il seguente snippet è un Activity di esempio che mostra l'utilizzo di
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") } } ) } } } }