A Navigation 3 apresenta um sistema flexível e poderoso para gerenciar o fluxo de interface do app usando cenas. Com as cenas, você pode criar layouts altamente personalizados, se adaptar a diferentes tamanhos de tela e gerenciar experiências complexas com vários painéis sem problemas.
Noções básicas sobre as cenas
No Navigation 3, um Scene
é a unidade fundamental que renderiza uma ou mais
instâncias de NavEntry
. Pense em uma Scene
como um estado ou uma seção visual distinta
da interface que pode conter e gerenciar a exibição de conteúdo da back
stack.
Cada instância Scene
é identificada de maneira exclusiva pelo key
e pela classe do
próprio Scene
. Esse identificador exclusivo é crucial porque ele direciona a
animação de nível superior quando o Scene
muda.
A interface Scene
tem as seguintes propriedades:
key: Any
: um identificador exclusivo para essa instância específica deScene
. Essa chave, combinada com a classeScene
, garante a distinção, principalmente para animações.entries: List<NavEntry<T>>
: é uma lista de objetosNavEntry
que oScene
é responsável por mostrar. É importante ressaltar que, se o mesmoNavEntry
for mostrado em váriasScenes
durante uma transição (por exemplo, em uma transição de elemento compartilhado), o conteúdo dele só será renderizado peloScene
de destino mais recente que o estiver exibindo.previousEntries: List<NavEntry<T>>
: essa propriedade define osNavEntry
s que seriam gerados se uma ação "voltar" ocorresse noScene
atual. É essencial para calcular o estado de retorno preditivo adequado, permitindo que oNavDisplay
antecipe e faça a transição para o estado anterior correto, que pode ser uma cena com uma classe e/ou chave diferentes.content: @Composable () -> Unit
: é a função combinável em que você define como oScene
renderiza oentries
e todos os elementos da interface ao redor específicos para esseScene
.
Entender as estratégias de cena
Um SceneStrategy
é o mecanismo que determina como uma determinada lista de
NavEntry
s da backstack precisa ser organizada e fazer a transição para uma
Scene
. Essencialmente, quando apresentado com as entradas atuais da backstack, um
SceneStrategy
faz duas perguntas importantes:
- Posso criar um
Scene
com essas entradas? Se oSceneStrategy
determinar que pode processar osNavEntry
s fornecidos e formar umScene
significativo, por exemplo, uma caixa de diálogo ou um layout de vários painéis, ele prossegue. Caso contrário, ele retornaránull
, dando a outras estratégias a chance de criar umScene
. - Em caso afirmativo, como organizar essas entradas no
Scene?
? Quando umSceneStrategy
se compromete a processar as entradas, ele assume a responsabilidade de criar umScene
e definir como asNavEntry
s especificadas serão exibidas nesseScene
.
O núcleo de uma SceneStrategy
é o método calculateScene
:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
Esse método recebe o List<NavEntry<T>>
atual da backstack e um
callback onBack
. Ele precisa retornar um Scene<T>
se puder formar um
com as entradas fornecidas ou null
se não puder.
O SceneStrategy
também fornece uma função infixa then
conveniente, permitindo
que você encadeie várias estratégias. Isso cria um pipeline de tomada de decisão flexível em que cada estratégia pode tentar calcular um Scene
e, se não for possível, delegar 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 sua backstack e
usa um SceneStrategy
para determinar e renderizar o Scene
apropriado.
O parâmetro NavDisplay's sceneStrategy
espera um SceneStrategy
que seja
responsável por calcular a Scene
a ser exibida. Se nenhuma Scene
for calculada
pela estratégia fornecida (ou cadeia de estratégias), a NavDisplay
vai usar automaticamente
uma SinglePaneSceneStrategy
por padrão.
Confira os detalhes da interação:
- Quando você adiciona ou remove chaves da backstack (por exemplo, usando
backStack.add()
oubackStack.removeLastOrNull()
), oNavDisplay
observa essas mudanças. - O
NavDisplay
transmite a lista atual deNavEntrys
(derivada das chaves de backstack) para o métodoSceneStrategy's calculateScene
configurado. - Se o
SceneStrategy
retornar umScene
, oNavDisplay
renderizará ocontent
desseScene
. ONavDisplay
também gerencia animações e retornos preditivos com base nas propriedades doScene
.
Exemplo: layout de painel único (comportamento padrão)
O layout personalizado mais simples que você pode ter é 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: 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) ) }
Exemplo: layout básico de duas guias (cena e estratégia personalizadas)
Este exemplo demonstra como criar um layout simples de duas guias que é ativado com base em duas condições:
- A largura da janela é suficientemente ampla para oferecer suporte a dois painéis (ou seja, pelo
menos
WIDTH_DP_MEDIUM_LOWER_BOUND
). - As duas primeiras entradas na backstack declaram explicitamente o suporte para serem exibidas 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.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 esse TwoPaneSceneStrategy
na NavDisplay
, modifique as
chamadas entryProvider
para incluir metadados TwoPaneScene.twoPane()
para as
entradas que você pretende mostrar em um layout de duas guias. Em seguida, forneça
TwoPaneSceneStrategy()
como sceneStrategy
, usando o substituto
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 detalhes e listas em uma cena adaptável do Material Design
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 (listas, 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 de lista de materiais, siga estas etapas:
- Adicione a dependência: inclua
androidx.compose.material3.adaptive:adaptive-navigation3
no arquivobuild.gradle.kts
do projeto. - Defina as entradas com metadados
ListDetailSceneStrategy
: uselistPane(), detailPane()
eextraPane()
para marcar oNavEntrys
para a exibição adequada do painel. O auxiliarlistPane()
também permite especificar umdetailPlaceholder
quando nenhum item é selecionado. - Usar
rememberListDetailSceneStrategy
(): essa função combinável fornece umListDetailSceneStrategy
pré-configurado que pode ser usado por umNavDisplay
.
O snippet abaixo é 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") } } ) } } } }