O Navigation 3 apresenta um sistema eficiente e flexível para gerenciar o fluxo da interface do seu app usando Cenas. Com as cenas, é possível criar layouts altamente personalizados, adaptar-se a diferentes tamanhos de tela e gerenciar experiências complexas de vários painéis sem problemas.
Noções básicas sobre as cenas
Na Navegação 3, um Scene é a unidade fundamental que renderiza uma ou mais instâncias de
NavEntry. Pense em um Scene como um estado visual ou uma seção
distinta da sua interface que pode conter e gerenciar a exibição de conteúdo da sua pilha
de retorno.
Cada instância Scene é identificada de maneira exclusiva pelo key e pela classe do
próprio Scene. Esse identificador exclusivo é essencial porque impulsiona a
animação de nível superior quando o Scene muda.
A interface Scene tem as seguintes propriedades:
key: Any: um identificador exclusivo para esta instância específica deScene. Essa chave, combinada com a classe doScene, garante a distinção, principalmente para fins de animação.entries: List<NavEntry<T>>: uma lista de objetosNavEntryque oSceneé responsável por mostrar. É importante lembrar que, se o mesmoNavEntryfor exibido em váriosScenesdurante uma transição (por exemplo, em uma transição de elemento compartilhado), o conteúdo dele só será renderizado peloScenede destino mais recente que o estiver mostrando.previousEntries: List<NavEntry<T>>: essa propriedade define osNavEntrys que resultariam se uma ação "voltar" ocorresse noSceneatual. É essencial para calcular o estado de retorno preditivo adequado, permitindo que oNavDisplayantecipe e faça a transição para o estado anterior correto, que pode ser uma cena com uma classe e/ou chave diferente.content: @Composable () -> Unit: é a função combinável em que você define como oScenerenderiza oentriese os elementos da interface específicos a esseScene.
Entender as estratégias de cena
Um SceneStrategy é o mecanismo que determina como uma determinada lista de
NavEntrys da pilha de retorno precisa ser organizada e transformada em uma
Scene. Basicamente, quando apresentada às entradas atuais da pilha de retorno, uma
SceneStrategy se faz duas perguntas principais:
- Posso criar um
Scenecom base nessas entradas? Se oSceneStrategydeterminar que pode processar osNavEntrys especificados e formar umScenesignificativo (por exemplo, uma caixa de diálogo ou um layout de vários painéis), ele vai continuar. Caso contrário, ele vai retornarnull, dando a outras estratégias a chance de criar umScene. - Se sim, como devo organizar essas entradas no
Scene?? Depois que umSceneStrategyse compromete a processar as entradas, ele assume a responsabilidade de construir umScenee definir como osNavEntrys especificados serão mostrados nesseScene.
O núcleo de um SceneStrategy é o método calculateScene:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
Esse método é uma função de extensão em um SceneStrategyScope que usa o
List<NavEntry<T>> atual da pilha de retorno. Ele precisa retornar um Scene<T>
se puder formar um com as entradas fornecidas ou null se não
puder.
O SceneStrategyScope é responsável por manter os argumentos opcionais
que o SceneStrategy pode precisar, como um callback onBack.
O SceneStrategy também oferece uma função infixa then conveniente, permitindo
encadear várias estratégias. Isso cria um pipeline flexível de tomada de decisões em que cada estratégia pode tentar calcular um Scene e, se não conseguir, delega para o próximo na cadeia.
Como as cenas e as estratégias de cena funcionam juntas
O NavDisplay é o elemento combinável central que observa a backstack e
usa um SceneStrategy para determinar e renderizar o Scene adequado.
O parâmetro NavDisplay's sceneStrategy espera um SceneStrategy responsável por calcular o Scene a ser mostrado. Se nenhum Scene for calculado pela estratégia (ou cadeia de estratégias) fornecida, NavDisplay vai usar automaticamente um SinglePaneSceneStrategy por padrão.
Confira um resumo da interação:
- Quando você adiciona ou remove chaves da pilha de retorno (por exemplo, usando
backStack.add()oubackStack.removeLastOrNull()), oNavDisplayobserva essas mudanças. - O
NavDisplaytransmite a lista atual deNavEntrys(derivada das chaves de pilha de retorno) ao métodoSceneStrategy's calculateSceneconfigurado. - Se o
SceneStrategyretornar umScene, oNavDisplayvai renderizar ocontentdesseScene. ONavDisplaytambém gerencia animações e volta preditiva com base nas propriedades doScene.
Exemplo: layout de painel único (comportamento padrão)
O layout personalizado mais simples é uma tela única, que é o comportamento padrão se nenhum outro SceneStrategy tiver precedência.
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) ) }
Exemplo: layout básico de dois painéis (cena e estratégia personalizadas)
Este exemplo mostra como criar um layout simples de dois painéis que é ativado com base em duas condições:
- A largura da janela é suficiente para acomodar dois painéis (ou seja, pelo menos
WIDTH_DP_MEDIUM_LOWER_BOUND). - As duas primeiras entradas na backstack declaram explicitamente o suporte para serem mostradas em um layout de dois painéis usando metadados específicos.
O snippet a seguir é o código-fonte combinado de 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 } } }
Para usar esse TwoPaneSceneStrategy na sua NavDisplay, modifique as chamadas de
entryProvider para incluir metadados TwoPaneScene.twoPane() nas
entradas que você pretende mostrar em um layout de dois painéis. Em seguida, forneça
TwoPaneSceneStrategy() como seu sceneStrategy, dependendo do retorno
padrão para cenários de painel único:
// 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() } } } ) }
Mostrar conteúdo de lista e detalhes em uma cena adaptável do Material
Para o caso de uso de detalhes e listas, o
artefato androidx.compose.material3.adaptive:adaptive-navigation3 fornece um
ListDetailSceneStrategy que cria um Scene de detalhes e listas. Esse Scene
processa automaticamente arranjos complexos de vários painéis (lista, detalhes e painéis
extras) e os adapta com base no tamanho da janela e no estado do dispositivo.
Para criar um Scene de lista-detalhe do Material, siga estas etapas:
- Adicione a dependência: inclua
androidx.compose.material3.adaptive:adaptive-navigation3no arquivobuild.gradle.ktsdo projeto. - Defina suas entradas com metadados
ListDetailSceneStrategy: uselistPane(), detailPane()eextraPane()para marcar seuNavEntryspara mostrar no painel apropriado. O helperlistPane()também permite especificar umdetailPlaceholderquando nenhum item está selecionado. - Use
rememberListDetailSceneStrategy(): essa função combinável fornece umListDetailSceneStrategypré-configurado que pode ser usado por umNavDisplay.
O snippet a seguir é um exemplo de Activity que demonstra o 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") } } ) } } } }