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. À medida que o usuário navega para frente até um novo conteúdo, ele é colocado na parte de cima da pilha. Quando ele volta desse conteúdo, ele é removido da pilha e o conteúdo anterior é mostrado. Em termos de navegação, essa pilha geralmente é chamada de backstack porque representa o conteúdo para o qual o usuário pode voltar.

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

Criar uma backstack

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

  • É simples navegar enviando chaves para a backstack.
  • Enquanto as chaves forem serializáveis, a backstack poderá ser salva no armazenamento persistente, permitindo que ela sobreviva a mudanças de configuração e à interrupção 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 na API Navigation 3 é que você é o proprietário da backstack. A biblioteca:

  • Espera que a backstack seja uma List<T> com suporte ao estado de snapshot, em que T é o tipo das keys da backstack. Você pode usar Any ou fornecer suas próprias chaves com tipo mais forte. Quando você vê os termos "push" ou "pop", a implementação subjacente é adicionar ou remover itens do final de uma lista.
  • Observa a 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. Ela representa um destino , uma única parte do conteúdo para a qual o usuário pode navegar para frente e para trás.

Um NavEntry também pode conter metadados, informações sobre o conteúdo. Esses metadados podem ser lidos por objetos de contêiner, como NavDisplay, para ajudar 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. Os metadata do NavEntry são um mapa de chaves String para valores Any, fornecendo armazenamento de dados versátil.

Para converter uma key em um NavEntry, crie um provedor de entrada. Essa é uma função que aceita uma key e retorna um NavEntry para essa key. Geralmente, ela é definida 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 entryProvider DSL.

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 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 entryProvider

A DSL entryProvider pode simplificar sua função lambda, evitando a necessidade de testar cada um dos tipos de chave e construir um NavEntry para cada um deles. Use a função de builder entryProvider para isso. Ela também inclui o 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 seu app. Sempre que a backstack muda, a interface do app precisa refletir o novo estado da backstack. Na Navigation 3, um NavDisplay observa a backstack e atualiza a interface de acordo com ela. Construa-o com os seguintes parâmetros:

  • Sua backstack: precisa ser do tipo SnapshotStateList<T>, em que T é o tipo das chaves da backstack. É uma List observável para que acione a recomposição de NavDisplay quando mudar.
  • Um entryProvider para converter as chaves na backstack em objetos NavEntry.
  • Opcionalmente, forneça um lambda ao 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 mais alto na backstack em um layout de painel único. A gravação a seguir mostra esse app em execução:

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

Ciclo de vida do destino

NavDisplay usa personalizados LifecycleOwners para limitar o estado do ciclo de vida de um NavEntry com base em restrições no nível da cena e da 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

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

Para cenas não sobrepostas:

  • RESUMED: permitido apenas quando a transição de cena for concluída e não houver cenas de sobreposição ativas sendo mostradas na parte de cima.
  • STARTED: limitado a STARTED durante as transições de cena, como ao navegar para frente ou para trás, ou quando estiver 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 cenas de sobreposição subjacentes cobertas por uma sobreposição mais recente.

Estado do ciclo de vida no nível da entrada

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

  • RESUMED: se a entrada estiver presente na backstack atual, o ciclo de vida dela poderá chegar a RESUMED (sujeito ao limite no 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 o ciclo de vida dela a CREATED. Esse limite garante que as entradas em segundo plano ou de saída parem de executar trabalhos ativos, como coletar fluxos ou iniciar corrotinas vinculadas a estados RESUMED ou STARTED, enquanto terminam 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 no nível da entrada 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 Navegação 3.
Figura 3. Diagrama mostrando como os dados fluem por vários objetos na Navigation 3.
  1. Os eventos de navegação iniciam mudanças. As chaves são adicionadas ou removidas da backstack em resposta a 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 da 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 é mostrado. O NavDisplay recebe o NavEntry e mostra o conteúdo.