Entender e implementar os conceitos básicos

A navegação descreve a forma como os usuários se movem pelo app. Eles interagem com elementos da interface, geralmente tocando ou clicando neles, e o app responde mostrando um novo conteúdo. Se o usuário quiser voltar ao conteúdo anterior, ele usa o gesto de volta ou toca no botão "Voltar".

Como modelar o estado de navegação

Uma maneira conveniente de modelar esse comportamento é com uma pilha de conteúdo. Conforme o usuário navega para frente em direção a um novo conteúdo, ele é enviado para o topo da pilha. Quando o usuário volta desse conteúdo, ele é retirado da pilha e o conteúdo anterior é exibido. Em termos de navegação, essa pilha geralmente é chamada de backstack porque representa o conteúdo que o usuário pode acessar novamente.

Um botão de ação do teclado de software (um ícone de verificação) circulado em vermelho.
Figura 1. Diagrama mostrando como a backstack muda com os eventos de navegação do usuário.

Criar uma backstack

Na Navigation 3, a backstack não contém conteúdo. Em vez disso, ele contém referências ao conteúdo, conhecidas como chaves. As chaves podem ser de qualquer tipo, mas geralmente são classes de dados simples e serializáveis. O uso de referências em vez de conteúdo tem os seguintes benefícios:

  • É simples navegar enviando chaves para a backstack.
  • Desde que as chaves sejam serializáveis, a backstack pode ser salva no armazenamento permanente, o que permite que ela sobreviva a mudanças de configuração e ao encerramento do processo. Isso é importante porque os usuários esperam sair do app, voltar mais tarde e continuar de onde pararam, com o mesmo conteúdo sendo mostrado. Consulte Salvar a backstack para mais informações.

Um conceito importante da API Navigation 3 é que você é o proprietário da backstack. A biblioteca:

  • Espera que o backstack seja um List<T> com suporte de estado de snapshot, em que T é o tipo do backstack keys. Você pode usar Any ou fornecer suas próprias chaves mais fortemente tipadas. Quando você vê os termos "push" ou "pop", a implementação subjacente é adicionar ou remover itens do final de uma lista.
  • Observa sua backstack e reflete o estado dela na interface usando um NavDisplay.

O exemplo a seguir mostra como criar chaves e uma backstack e como modificar a backstack em resposta a eventos de navegação do usuário:

// Define keys that will identify content
data object ProductList
data class ProductDetail(val id: String)

@Composable
fun MyApp() {

    // Create a back stack, specifying the key the app should start with
    val backStack = remember { mutableStateListOf<Any>(ProductList) }

    // Supply your back stack to a NavDisplay so it can reflect changes in the UI
    // ...more on this below...

    // Push a key onto the back stack (navigate forward), the navigation library will reflect the change in state
    backStack.add(ProductDetail(id = "ABC"))

    // Pop a key off the back stack (navigate back), the navigation library will reflect the change in state
    backStack.removeLastOrNull()
}

Resolver chaves para conteúdo

O conteúdo é modelado na Navigation 3 usando NavEntry, que é uma classe que contém uma função combinável. Ele representa um destino, ou seja, um único conteúdo que o usuário pode acessar e sair.

Um NavEntry também pode conter metadados, que são informações sobre o conteúdo. Esses metadados podem ser lidos por objetos de contêiner, como NavDisplay, para ajudá-los a decidir como mostrar o conteúdo do NavEntry. Por exemplo, os metadados podem ser usados para substituir as animações padrão de um NavEntry específico. NavEntry metadata é um mapa de chaves String para valores Any, oferecendo armazenamento de dados versátil.

Para converter um key em um NavEntry, crie um provedor de entrada. Essa é uma função que aceita um key e retorna um NavEntry para esse key. Ele geralmente é definido como um parâmetro lambda ao criar um NavDisplay.

Há duas maneiras de criar um provedor de entrada: criando uma função lambda diretamente ou usando a DSL entryProvider.

Criar uma função de provedor de entrada diretamente

Normalmente, você cria uma função de provedor de entrada usando uma instrução when, com uma ramificação para cada uma das suas chaves.

entryProvider = { key ->
    when (key) {
        is ProductList -> NavEntry(key) { Text("Product List") }
        is ProductDetail -> NavEntry(
            key,
            metadata = mapOf("extraDataKey" to "extraDataValue")
        ) { Text("Product ${key.id} ") }

        else -> {
            NavEntry(Unit) { Text(text = "Invalid Key: $it") }
        }
    }
}

Usar a DSL do entryProvider

A DSL entryProvider pode simplificar sua função lambda, evitando a necessidade de testar cada um dos tipos de chave e criar um NavEntry para cada um. Use a função de builder entryProvider para isso. Ele também inclui um comportamento de fallback padrão (gerar um erro) se a chave não for encontrada.

entryProvider = entryProvider {
    entry<ProductList> { Text("Product List") }
    entry<ProductDetail>(
        metadata = mapOf("extraDataKey" to "extraDataValue")
    ) { key -> Text("Product ${key.id} ") }
}

Observe o seguinte no snippet:

  • entry é usado para definir um NavEntry com o tipo e o conteúdo combinável especificados.
  • entry aceita um parâmetro metadata para definir NavEntry.metadata.

Mostrar a backstack

A backstack representa o estado de navegação do app. Sempre que a backstack mudar, a interface do app precisará refletir o novo estado dela. No Navigation 3, um NavDisplay observa sua backstack e atualiza a interface de acordo. Crie-o com os seguintes parâmetros:

  • Seu backstack, que precisa ser do tipo SnapshotStateList<T>, em que T é o tipo das chaves do backstack. É um List observável para que ele acione a recomposição de NavDisplay quando mudar.
  • Uma entryProvider para converter as chaves na backstack em objetos NavEntry.
  • Se quiser, forneça uma lambda para o parâmetro onBack. Ele é chamado quando o usuário aciona um evento de retorno.

O exemplo a seguir mostra como criar um NavDisplay.

data object Home
data class Product(val id: String)

@Composable
fun NavExample() {

    val backStack = remember { mutableStateListOf<Any>(Home) }

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        entryProvider = { key ->
            when (key) {
                is Home -> NavEntry(key) {
                    ContentGreen("Welcome to Nav3") {
                        Button(onClick = {
                            backStack.add(Product("123"))
                        }) {
                            Text("Click to navigate")
                        }
                    }
                }

                is Product -> NavEntry(key) {
                    ContentBlue("Product ${key.id} ")
                }

                else -> NavEntry(Unit) { Text("Unknown route") }
            }
        }
    )
}

Por padrão, o NavDisplay mostra o NavEntry de cima na backstack em um layout de painel único. A gravação a seguir mostra esse app em execução:

Comportamento padrão de `NavDisplay` com dois
destinos.
Figura 2. NavDisplay: comportamento padrão com dois destinos.

Ciclo de vida do destino

O NavDisplay usa LifecycleOwners personalizados para limitar o estado do ciclo de vida de um NavEntry com base nas restrições de nível de Scene e de entrada.

Para mais informações sobre ciclos de vida no Compose, consulte Ciclo de vida no Jetpack Compose.

Restrições de ciclo de vida no nível da cena

O NavDisplay gerencia o ciclo de vida das Scenes ativas. Os limites no nível da cena são determinados da seguinte forma:

Para cenas sem sobreposição:

  • RESUMED: permitido apenas quando a transição de cena é concluída e não há cenas de sobreposição ativas sendo mostradas sobre ela.
  • STARTED: limitado a STARTED durante as transições de cena, como ao navegar para frente ou para trás ou quando é coberto por uma sobreposição.

Para cenas de sobreposição, como caixas de diálogo ou páginas inferiores:

  • RESUMED: permitido apenas para a cena de sobreposição mais alta e ativa no momento.
  • STARTED: limitado a STARTED para qualquer cena de sobreposição subjacente que esteja coberta por uma sobreposição mais recente.

Estado do ciclo de vida básico

A biblioteca gerencia o estado máximo do ciclo de vida de cada NavEntry com base na presença dele na backstack:

  • RESUMED: se a entrada estiver presente na backstack atual, o ciclo de vida dela poderá ir até RESUMED (sujeito ao limite do nível da cena).
  • CREATED: se a entrada não estiver mais na backstack, como quando ela foi removida, mas ainda está sendo renderizada na tela durante a animação, a biblioteca limita estritamente o ciclo de vida dela a CREATED. Esse limite garante que as entradas em segundo plano ou de saída parem de executar o trabalho ativo, como coletar fluxos ou iniciar corrotinas vinculadas aos estados RESUMED ou STARTED enquanto concluem as transições de saída.

Como eles se combinam

Por exemplo, o estado final do ciclo de vida de um NavEntry é resolvido da seguinte maneira:

Cenário Limite no nível da cena Limite de nível básico Limite efetivo
Entrada ativa, tela definida (sem transições ou sobreposições) RESUMED RESUMED RESUMED
Entrada ativa, durante a transição (navegando para ou de) STARTED RESUMED STARTED
Entrada ativa, coberta por uma sobreposição (por exemplo, uma caixa de diálogo está aberta) STARTED RESUMED STARTED
Entrada removida, animação de saída STARTED ou RESUMED CREATED CREATED

Como tudo funciona em conjunto

O diagrama a seguir mostra como os dados fluem entre os vários objetos na Navigation 3:

Uma visualização de como os dados fluem entre os vários objetos na Navigation 3.
Figura 3. Diagrama mostrando como os dados fluem por vários objetos no Navigation 3.
  1. Os eventos de navegação iniciam as mudanças. As chaves são adicionadas ou removidas da backstack em resposta às interações do usuário.

  2. A mudança no estado da backstack aciona a recuperação de conteúdo. O NavDisplay (um elemento combinável que renderiza uma backstack) observa a backstack. Na configuração padrão, ele mostra a entrada de backstack mais alta em um layout de painel único. Quando a chave superior na backstack muda, o NavDisplay usa essa chave para solicitar o conteúdo correspondente do provedor de entrada.

  3. O provedor de entrada fornece conteúdo. O provedor de entrada é uma função que resolve uma chave para um NavEntry. Ao receber uma chave do NavDisplay, o provedor de entrada fornece o NavEntry associado, que contém a chave e o conteúdo.

  4. O conteúdo é exibido. O NavDisplay recebe o NavEntry e mostra o conteúdo.