Navigazione 3 introduce un sistema potente e flessibile per gestire il flusso dell'UI dell'app tramite le scene. Le scene ti consentono di creare layout altamente personalizzati, adattarti a diverse dimensioni dello schermo e gestire facilmente 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 NavEntry
. Pensa a un Scene
come a uno stato visivo o a una sezione distinta della UI che può contenere e gestire la visualizzazione dei contenuti dall'elenco precedente.
Ogni istanza Scene
è identificata in modo univoco dal proprio key
e dalla classe del
Scene
stesso. Questo identificatore univoco è fondamentale perché gestisce 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 distintività, principalmente ai fini dell'animazione.entries: List<NavEntry<T>>
: questo è un elenco di oggettiNavEntry
cheScene
è responsabile di mostrare. È importante sottolineare che se lo stessoNavEntry
viene visualizzato in piùScenes
durante una transizione (ad es. in una transizione di elementi condivisi), i relativi contenuti verranno visualizzati solo dall'Scene
di destinazione più recente che lo mostra.previousEntries: List<NavEntry<T>>
: questa proprietà definisce i valoriNavEntry
che si otterrebbero se si verifica un'azione "Indietro" dal valoreScene
corrente. È essenziale per calcolare lo stato di ritorno predittivo corretto, consentendo aNavDisplay
di anticipare e passare allo stato precedente corretto, che può essere una scena con una classe e/o una chiave diversa.content: @Composable () -> Unit
: questa è la funzione componibile in cui definisci in che modoScene
esegue il rendering del proprioentries
e di eventuali elementi dell'interfaccia utente adiacenti specifici per quelScene
.
Informazioni sulle strategie di scena
Un SceneStrategy
è il meccanismo che determina in che modo un determinato elenco di NavEntry
dello stack posteriore deve essere organizzato e trasferito in un Scene
. In sostanza, quando viene presentato con le voci attuali della pila di ritorno, un
SceneStrategy
si pone due domande chiave:
- Posso creare un
Scene
da queste voci? Se ilSceneStrategy
stabilisce di poter gestire iNavEntry
specificati e formare unScene
sensato (ad es. una finestra di dialogo o un layout a più riquadri), procede. In caso contrario, restituiscenull
, dando alle altre strategie la possibilità di creare unScene
. - In questo caso, come devo organizzare queste voci in
Scene?
? Una volta che unSceneStrategy
si impegna a gestire le voci, si assume la responsabilità di creare unScene
e di definire la modalità di visualizzazione deiNavEntry
specificati all'interno di questoScene
.
Il nucleo di un SceneStrategy
è il suo metodo calculateScene
:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
Questo metodo prende l'List<NavEntry<T>>
corrente dallo stack di ritorno e un callback onBack
. Deve restituire Scene<T>
se riesce a formarne uno
dalle voci fornite o null
se non ci riesce.
SceneStrategy
fornisce anche una comoda funzione infix then
, che consente di concatenare più strategie. In questo modo viene creata una pipeline di elaborazione delle decisioni flessibile in cui ogni strategia può tentare di calcolare un Scene
e, se non ci riesce, delega la procedura alla strategia successiva della catena.
Come funzionano insieme le scene e le strategie di scena
NavDisplay
è il composable centrale che osserva la pila precedente e utilizza un SceneStrategy
per determinare e visualizzare il Scene
appropriato.
Il parametro NavDisplay's sceneStrategy
prevede un SceneStrategy
responsabile del calcolo del Scene
da visualizzare. Se non viene calcolato alcun valore Scene
dalla strategia (o dalla catena di strategie) specificata, per NavDisplay
viene usato automaticamente un valore SinglePaneSceneStrategy
per impostazione predefinita.
Ecco una suddivisione dell'interazione:
- Quando aggiungi o rimuovi chiavi dallo stack precedente (ad es. utilizzando
backStack.add()
obackStack.removeLastOrNull()
),NavDisplay
osserva queste modifiche. NavDisplay
passa l'elenco corrente diNavEntrys
(derivato dalle chiavi della pila interna) al metodoSceneStrategy's calculateScene
configurato.- Se
SceneStrategy
restituisce correttamente unScene
,NavDisplay
poi esegue il rendering delcontent
di quelScene
.NavDisplay
gestisce anche le animazioni e il ritorno predittivo in base alle proprietà diScene
.
Esempio: layout a riquadro singolo (comportamento predefinito)
Il layout personalizzato più semplice che puoi avere è una visualizzazione a riquadro singolo, che è il comportamento predefinito se nessun altro SceneStrategy
ha la precedenza.
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) ) }
Esempio: layout a due riquadri di base (scena e strategia personalizzate)
Questo esempio mostra come creare un semplice layout a due riquadri 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 nella pila posteriore dichiarano esplicitamente il loro 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.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 } } }
Per utilizzare questo TwoPaneSceneStrategy
in 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
, facendo affidamento sul valore predefinito per gli scenari con riquadro singolo:
// 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() } } } ) }
Mostrare i contenuti dei dettagli dell'elenco in una scena Material Adaptive
Per il caso d'uso elenco-dettaglio, l'elemento androidx.compose.material3.adaptive:adaptive-navigation3
fornisce un ListDetailSceneStrategy
che crea un Scene
elenco-dettaglio. Questa Scene
gestisce automaticamente disposizioni complesse con più riquadri (elenchi, riquadri dettagliati e aggiuntivi) e li adatta in base alle dimensioni della finestra e allo stato del dispositivo.
Per creare un dettaglio elenco di materiali Scene
:
- Aggiungi la dipendenza: includi
androidx.compose.material3.adaptive:adaptive-navigation3
nel filebuild.gradle.kts
del progetto. - Definisci le voci con i metadati
ListDetailSceneStrategy
: utilizzalistPane(), detailPane()
eextraPane()
per contrassegnareNavEntrys
per la visualizzazione del riquadro appropriata. L'helperlistPane()
ti consente anche di specificare un valoredetailPlaceholder
quando non è selezionato alcun elemento. - Utilizza
rememberListDetailSceneStrategy
(): questa funzione componibile fornisce unListDetailSceneStrategy
preconfigurato 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") } } ) } } } }