Navigation 3 presenta un sistema potente y flexible para administrar el flujo de la IU de tu app a través de escenas. Las escenas te permiten crear diseños altamente personalizados, adaptarte a diferentes tamaños de pantalla y administrar experiencias complejas de varios paneles sin problemas.
Comprende las escenas
En Navigation 3, un Scene
es la unidad fundamental que renderiza una o más instancias de NavEntry
. Piensa en un Scene
como un estado visual o una sección distinta de tu IU que puede contener y administrar la visualización de contenido desde tu pila de actividades.
Cada instancia de Scene
se identifica de forma única por su key
y la clase del
Scene
. Este identificador único es fundamental porque impulsa la animación de nivel superior cuando cambia Scene
.
La interfaz Scene
tiene las siguientes propiedades:
key: Any
: Es un identificador único para esta instancia específica deScene
. Esta clave, combinada con la clase deScene
, garantiza la distinción, principalmente para fines de animación.entries: List<NavEntry<T>>
: Es una lista de objetosNavEntry
queScene
es responsable de mostrar. Es importante que, si el mismoNavEntry
se muestra en variosScenes
durante una transición (p.ej., en una transición de elementos compartidos), su contenido solo se renderizará en elScene
de destino más reciente que lo muestre.previousEntries: List<NavEntry<T>>
: Esta propiedad define losNavEntry
que se generarían si se produce una acción "atrás" desde elScene
actual. Es esencial para calcular el estado de atrás predictivo correcto, lo que permite queNavDisplay
anticipe y realice la transición al estado anterior correcto, que puede ser una escena con una clase o clave diferente.content: @Composable () -> Unit
: Esta es la función componible en la que defines cómoScene
renderiza suentries
y cualquier elemento de la IU circundante específico de eseScene
.
Comprende las estrategias de escenas
Un SceneStrategy
es el mecanismo que determina cómo se debe organizar una lista determinada de NavEntry
de la pila de actividades y cómo se debe realizar la transición a un Scene
. En esencia, cuando se le presentan las entradas de pila de actividades actuales, un SceneStrategy
se hace dos preguntas clave:
- ¿Puedo crear un
Scene
a partir de estas entradas? Si elSceneStrategy
determina que puede controlar losNavEntry
dados y formar unScene
significativo (p.ej., un diálogo o un diseño de varios paneles), continúa. De lo contrario, muestranull
, lo que les brinda a otras estrategias la oportunidad de crear unScene
. - Si es así, ¿cómo debo organizar esas entradas en el
Scene?
? Una vez que unSceneStrategy
se compromete a controlar las entradas, asume la responsabilidad de construir unScene
y definir cómo se mostrarán losNavEntry
especificados dentro de eseScene
.
El núcleo de un SceneStrategy
es su método calculateScene
:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
Este método toma el List<NavEntry<T>>
actual de la pila de actividades y una devolución de llamada onBack
. Debe mostrar un Scene<T>
si puede formar uno correctamente a partir de las entradas proporcionadas o null
si no puede hacerlo.
SceneStrategy
también proporciona una función infijo then
conveniente, lo que te permite encadenar varias estrategias. Esto crea una canalización de toma de decisiones flexible en la que cada estrategia puede intentar calcular un Scene
y, si no puede, delega a la siguiente en la cadena.
Cómo funcionan en conjunto las escenas y las estrategias de escenas
NavDisplay
es el elemento componible central que observa tu pila de actividades y usa un SceneStrategy
para determinar y renderizar el Scene
adecuado.
El parámetro NavDisplay's sceneStrategy
espera un SceneStrategy
que sea responsable de calcular el Scene
que se mostrará. Si la estrategia proporcionada (o la cadena de estrategias) no calcula ningún Scene
, NavDisplay
recurre automáticamente a un SinglePaneSceneStrategy
de forma predeterminada.
A continuación, se muestra un desglose de la interacción:
- Cuando agregas o quitas claves de la pila de actividades (p.ej., con
backStack.add()
obackStack.removeLastOrNull()
),NavDisplay
observa estos cambios. NavDisplay
pasa la lista actual deNavEntrys
(derivada de las claves de la pila de actividades) al métodoSceneStrategy's calculateScene
configurado.- Si
SceneStrategy
muestra correctamente unScene
,NavDisplay
renderiza elcontent
de eseScene
.NavDisplay
también administra animaciones y el gesto atrás predictivo según las propiedades deScene
.
Ejemplo: Diseño de un solo panel (comportamiento predeterminado)
El diseño personalizado más simple que puedes tener es una pantalla de un solo panel, que es el comportamiento predeterminado si no tiene prioridad ningún otro SceneStrategy
.
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) ) }
Ejemplo: Diseño básico de dos paneles (escenario y estrategia personalizados)
En este ejemplo, se muestra cómo crear un diseño simple de dos paneles que se activa según dos condiciones:
- El ancho de la ventana es lo suficientemente amplio como para admitir dos paneles (es decir, al menos
WIDTH_DP_MEDIUM_LOWER_BOUND
). - Las dos entradas principales de la pila de actividades declaran de forma explícita su compatibilidad para mostrarse en un diseño de dos paneles con metadatos específicos.
El siguiente fragmento es el código fuente combinado de TwoPaneScene.kt
y 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 } } }
Para usar este TwoPaneSceneStrategy
en tu NavDisplay
, modifica tus llamadas a entryProvider
para incluir metadatos TwoPaneScene.twoPane()
para las entradas que deseas mostrar en un diseño de dos paneles. Luego, proporciona TwoPaneSceneStrategy()
como tu sceneStrategy
, confíe en el resguardo predeterminado para situaciones de un solo panel:
// 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() } } } ) }
Cómo mostrar contenido de lista-detalles en una escena adaptable de Material
Para el caso de uso de lista-detalles, el artefacto androidx.compose.material3.adaptive:adaptive-navigation3
proporciona un ListDetailSceneStrategy
que crea un Scene
de lista-detalles. Este Scene
controla automáticamente las disposiciones complejas de varios paneles (listas, paneles de detalles y paneles adicionales) y las adapta según el tamaño de la ventana y el estado del dispositivo.
Para crear un Scene
de lista de materiales de detalles, sigue estos pasos:
- Agrega la dependencia: Incluye
androidx.compose.material3.adaptive:adaptive-navigation3
en el archivobuild.gradle.kts
de tu proyecto. - Define tus entradas con metadatos
ListDetailSceneStrategy
: UsalistPane(), detailPane()
yextraPane()
para marcar tuNavEntrys
para la visualización adecuada del panel. El ayudantelistPane()
también te permite especificar undetailPlaceholder
cuando no se selecciona ningún elemento. - Usa
rememberListDetailSceneStrategy
(): Esta función de componibilidad proporciona unListDetailSceneStrategy
preconfigurado que puede usar unNavDisplay
.
El siguiente fragmento es un Activity
de muestra que demuestra el uso de 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") } } ) } } } }