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 seção distinta da sua interface que pode conter e gerenciar a exibição de conteúdo da sua backstack.
Cada instância Scene é identificada de maneira exclusiva pelo key e pela classe do
próprio Scene. Esse identificador exclusivo é crucial 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 mostrado 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 exibindo.previousEntries: List<NavEntry<T>>: essa propriedade define osNavEntrys que resultariam se uma ação "voltar" ocorresse noSceneatual. É essencial para calcular o estado de volta preditiva 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 desseScene.metadata: Map<String, Any>: fornece informações específicas da cena a outros componentes da biblioteca, comoNavDisplay. Por padrão, retorna ometadatado últimoNavEntryementries.
Entender as estratégias de cena
Um SceneStrategy é o mecanismo que determina como uma determinada lista de
NavEntrys da backstack deve ser organizada e transformada em um
Scene. Basicamente, quando apresentada às entradas atuais do backstack, uma
SceneStrategy se faz duas perguntas principais:
- Posso criar um
Scenecom essas 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 do backstack. 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.
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 detalhamento da interação:
- Quando você adiciona ou remove chaves do backstack (por exemplo, usando
backStack.add()oubackStack.removeLastOrNull()), oNavDisplayobserva essas mudanças. - O
NavDisplaytransmite a lista atual deNavEntrys(derivada das chaves de backstack) ao métodoSceneStrategy's calculateSceneconfigurado. - Se o
SceneStrategyretornar umScene, oNavDisplayvai renderizar ocontentdesseScene. ONavDisplaytambém gerencia animações e a volta preditiva com base nas propriedades doScene.
Exemplo: layout de painel único (comportamento padrão)
O layout personalizado mais simples é uma tela de painel único, 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> { override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? = SinglePaneScene( key = entries.last().contentKey, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
Exemplo: layout básico de detalhes e listas (cena e estratégia personalizadas)
Este exemplo mostra como criar um layout simples de detalhes e listas 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). - A backstack contém entradas que declararam suporte para serem mostradas em um layout de detalhes e listas usando metadados específicos.
O snippet a seguir é o código-fonte de ListDetailScene.kt e contém ListDetailScene e 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.contains(DetailKey) } ?: return null val listEntry = entries.findLast { it.metadata.contains(ListKey) } ?: 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 ) } object ListKey : NavMetadataKey<Boolean> object DetailKey : NavMetadataKey<Boolean> companion object { /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun listPane() = metadata { put(ListKey, true) } /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun detailPane() = metadata { put(DetailKey, true) } } }
Para usar esse ListDetailSceneStrategy no seu NavDisplay, modifique as chamadas de entryProvider para incluir metadados ListDetailScene.listPane() da entrada que você quer mostrar como um layout de lista e o ListDetailScene.detailPane() da entrada que você quer mostrar como um layout de detalhe. Em seguida, forneça ListDetailSceneStrategy() como seu sceneStrategy,
usando o fallback padrão para cenários de painel único:
// 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() }, sceneStrategies = listOf(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) }
Se você não quiser criar sua própria cena de detalhes e listas, use a cena de detalhes e listas do Material, que vem com detalhes sensíveis e o suporte para marcadores de posição, conforme mostrado na próxima seção.
Mostrar conteúdo de detalhes e listas em uma cena adaptativa 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. Isso 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 detalhes e listas 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<NavKey>() NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { backStack.removeLastOrNull() }, sceneStrategies = listOf(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") } } ) } } } }