Navigation 3 presenta un sistema potente y flexible para administrar el flujo de la IU de tu app a través de Scenes. 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.
Información sobre 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 distintos de tu IU que pueden contener y administrar la visualización del contenido de tu pila de actividades.
Cada instancia de Scene se identifica de forma única por su key y la clase del propio 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 objetosNavEntryque elScenees responsable de mostrar. Es importante destacar que, si el mismoNavEntryse muestra en variosScenesdurante una transición (p.ej., en una transición de elementos compartidos), su contenido solo se renderizará en elScenede destino más reciente que lo muestre.previousEntries: List<NavEntry<T>>: Esta propiedad define losNavEntrys que se generarían si se realiza una acción de "volver" desde elSceneactual. Es fundamental para calcular el estado de atrás predictivo adecuado, lo que permite queNavDisplayanticipe y realice la transición al estado anterior correcto, que puede ser una escena con una clase o una clave diferente.content: @Composable () -> Unit: Esta es la función componible en la que defines cómo elScenerenderiza suentriesy cualquier elemento de IU circundante específico de eseScene.
Comprende las estrategias de escena
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. Básicamente, cuando se le presentan las entradas actuales de la pila de actividades, un SceneStrategy se hace dos preguntas clave:
- ¿Puedo crear un
Scenea partir de estas entradas? Si elSceneStrategydetermina que puede controlar losNavEntrys proporcionados y formar unScenesignificativo (p.ej., un diálogo o un diseño de varios paneles), continúa. De lo contrario, devuelvenull, lo que les da a otras estrategias la oportunidad de crear unScene. - Si es así, ¿cómo debo organizar esas entradas en el
Scene?? Una vez que unSceneStrategyse compromete a controlar las entradas, asume la responsabilidad de construir unSceney definir cómo se mostrarán losNavEntryespecificados 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 es una función de extensión en un SceneStrategyScope que toma el List<NavEntry<T>> actual de la pila de actividades. Debe devolver un Scene<T> si puede formar uno correctamente a partir de las entradas proporcionadas o null si no puede.
El SceneStrategyScope es responsable de mantener los argumentos opcionales que el SceneStrategy podría necesitar, como una devolución de llamada de onBack.
SceneStrategy también proporciona una conveniente función de infijo then, que te permite encadenar varias estrategias. Esto crea un canal de toma de decisiones flexible en el que cada estrategia puede intentar calcular un Scene y, si no puede, delega la tarea a la siguiente estrategia de la cadena.
Cómo funcionan en conjunto las escenas y las estrategias de escenas
El elemento 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 usar un SinglePaneSceneStrategy de forma predeterminada.
A continuación, se muestra un desglose de la interacción:
- Cuando agregas o quitas claves de tu pila de actividades (p.ej., con
backStack.add()obackStack.removeLastOrNull()),NavDisplayobserva estos cambios. - El
NavDisplaypasa la lista actual deNavEntrys(derivada de las claves de la pila de historial) al métodoSceneStrategy's calculateSceneconfigurado. - Si
SceneStrategydevuelve correctamente unScene, elNavDisplayrenderiza elcontentde eseScene. ElNavDisplaytambién administra las animaciones y el gesto de atrás predictivo según las propiedades delScene.
Ejemplo: Diseño de panel único (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 ningún otro SceneStrategy tiene prioridad.
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) ) }
Ejemplo: Diseño básico de lista-detalles (Scene y estrategia personalizadas)
En este ejemplo, se muestra cómo crear un diseño simple de lista y detalles que se activa según dos condiciones:
- El ancho de la ventana es lo suficientemente ancho como para admitir dos paneles (es decir, al menos
WIDTH_DP_MEDIUM_LOWER_BOUND). - La pila de actividades hacia atrás contiene entradas que declararon su compatibilidad para mostrarse en un diseño de lista y detalles con metadatos específicos.
El siguiente fragmento es el código fuente de ListDetailScene.kt y contiene ListDetailScene y 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.containsKey(DETAIL_KEY) } ?: return null val listEntry = entries.findLast { it.metadata.containsKey(LIST_KEY) } ?: 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 ) } companion object { internal const val LIST_KEY = "ListDetailScene-List" internal const val DETAIL_KEY = "ListDetailScene-Detail" /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun listPane() = mapOf(LIST_KEY to true) /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun detailPane() = mapOf(DETAIL_KEY to true) } }
Para usar este ListDetailSceneStrategy en tu NavDisplay, modifica tus llamadas a entryProvider para incluir metadatos de ListDetailScene.listPane() para la entrada que deseas mostrar como un diseño de lista y el ListDetailScene.detailPane() para la entrada que deseas mostrar como un diseño de detalle. Luego, proporciona ListDetailSceneStrategy() como tu sceneStrategy, y confía en la resiliencia predeterminada para situaciones de un solo panel:
// 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() }, sceneStrategy = 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) }
Si no quieres crear tu propia escena de lista-detalles, puedes usar la escena de lista-detalles de Material, que incluye detalles útiles y compatibilidad con marcadores de posición, como se muestra en la siguiente sección.
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 (lista, detalles y paneles adicionales) y los adapta según el tamaño de la ventana y el estado del dispositivo.
Para crear un Scene de lista-detalle de Material, sigue estos pasos:
- Agrega la dependencia: Incluye
androidx.compose.material3.adaptive:adaptive-navigation3en el archivobuild.gradle.ktsde tu proyecto. - Define tus entradas con metadatos
ListDetailSceneStrategy: UsalistPane(), detailPane()yextraPane()para marcar tuNavEntryspara que se muestre en el panel adecuado. El asistentelistPane()también te permite especificar undetailPlaceholdercuando no se selecciona ningún elemento. - Use
rememberListDetailSceneStrategy(): Esta función de componibilidad proporciona unListDetailSceneStrategypreconfigurado que puede usar unNavDisplay.
El siguiente fragmento es un ejemplo de Activity 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<NavKey>() NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { 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") } } ) } } } }