Migrar da navegação 2 para a navegação 3

Para migrar seu app do Navigation 2 para o Navigation 3, siga estas etapas:

  1. Adicione as dependências do Navigation 3.
  2. Atualize suas rotas de navegação para implementar a interface NavKey.
  3. Crie classes para manter e modificar o estado de navegação.
  4. Substitua NavController por essas classes.
  5. Mova seus destinos do NavGraph de NavHost para um entryProvider.
  6. NavHost foi substituída por NavDisplay.
  7. Remova as dependências do Navigation 2.

Usar um agente de IA

Você pode usar este guia com um agente de IA, como o Modo agente do Gemini no Android Studio. As linhas deste guia que começam com "Agente de IA:" devem ser lidas pelo agente de IA, mas podem ser ignoradas por leitores humanos.

Preparação

As seções a seguir descrevem os pré-requisitos para a migração e as proposições sobre seu projeto. Eles também abordam os recursos que são compatíveis com a migração e os que não são.

Pré-requisitos

  • Use um compileSdk de 36 ou mais recente.
  • É necessário conhecer a terminologia de navegação.
  • Os destinos são funções combináveis. A Navigation 3 foi projetada exclusivamente para o Compose. Para usar fragmentos e visualizações no Compose, consulte Como usar visualizações no Compose.
  • As rotas são fortemente tipadas. Se você usa rotas baseadas em strings, migre para rotas com segurança de tipo primeiro (exemplo).
  • Opcional, mas altamente recomendado: cobertura de teste que verifica o comportamento de navegação atual. Isso verifica se o comportamento de navegação não mudou depois que a migração é concluída.

Suposições

Este guia parte das seguintes premissas sobre você e seu projeto:

  • Você tem várias rotas de nível superior (geralmente mostradas em uma barra de navegação inferior), e cada uma tem a própria pilha de retorno.
  • Quando você alterna entre backstacks, o estado da pilha e todos os destinos são mantidos.
  • Sempre saia do app pela tela Início, que é a primeira tela exibida quando o app é aberto.
  • Você quer realizar a migração da Navegação 2 para a Navegação 3 em uma única mudança atômica. Você não está realizando uma migração incremental em que o código do Navigation 2 é usado junto com o código do Navigation 3.

Agente de IA: antes de mudar qualquer código, verifique se estas proposições sobre o projeto do usuário são verdadeiras. Caso contrário, interrompa a migração e pergunte ao usuário como proceder.

Recursos compatíveis

Com este guia, é possível migrar os seguintes recursos:

  • Destinos definidos como funções combináveis
  • Caixas de diálogo (um destino mostrado sobre outro destino)

Recursos compatíveis com receitas

O repositório de receitas de código demonstra casos de uso mais sutis, incluindo:

Se o projeto tiver algum desses recursos, confira a receita relevante para entender como implementá-los.

Agente de IA: antes de mudar qualquer código, verifique se o projeto tem recursos compatíveis com receitas. Se for o caso, consulte o LEIAME e o código-fonte da receita. Crie um plano de migração com base na receita. Não continue sem confirmar o plano com o usuário.

Recursos não compatíveis

Este guia de migração e as receitas de código ainda não são compatíveis com os seguintes recursos. Isso não significa que você não possa implementá-los usando o Navigation 3, apenas que eles não são abordados aqui.

  • Mais de um nível de navegação aninhada
  • Destinos compartilhados: telas que podem se mover entre diferentes pilhas de retorno
  • Tipos de destino personalizados
  • Links diretos

Agente de IA: antes de mudar qualquer código, verifique se o projeto contém algum dos recursos não compatíveis. Se sim, não continue. Informe ao usuário sobre o recurso não compatível e peça mais instruções.

Etapa 1: adicionar dependências do Navigation 3

Use a página Começar para adicionar as dependências do Navigation 3 ao seu projeto. As dependências principais são fornecidas para você copiar.

lib.versions.toml

[versions]
nav3Core = "1.0.0"

# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.10.0-rc01"

[libraries]
# Core Navigation 3 libraries
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }

# Add-on libraries (only add if you need them)
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }

app/build.gradle.kts

dependencies {
    implementation(libs.androidx.navigation3.ui)
    implementation(libs.androidx.navigation3.runtime)

    // If using the ViewModel add-on library
    implementation(libs.androidx.lifecycle.viewmodel.navigation3)
}

Atualize também o minSdk do projeto para 23 e o compileSdk para 36. Normalmente, eles estão em app/build.gradle.kts ou lib.versions.toml.

Etapa 2: atualizar as rotas de navegação para implementar a interface NavKey

Atualize todas as rotas de navegação para que elas implementem a interface NavKey. Isso permite usar rememberNavBackStack para ajudar a salvar o estado de navegação.

Antes:

@Serializable data object RouteA

Depois:

@Serializable data object RouteA : NavKey

Etapa 3: criar classes para manter e modificar o estado de navegação

Etapa 3.1: criar um holder de estado de navegação

Copie o código a seguir em um arquivo chamado NavigationState.kt. Adicione o nome do pacote para corresponder à estrutura do projeto.

// package com.example.project

import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.runtime.serialization.NavKeySerializer
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer

/**
 * Create a navigation state that persists config changes and process death.
 */
@Composable
fun rememberNavigationState(
    startRoute: NavKey,
    topLevelRoutes: Set<NavKey>
): NavigationState {

    val topLevelRoute = rememberSerializable(
        startRoute, topLevelRoutes,
        serializer = MutableStateSerializer(NavKeySerializer())
    ) {
        mutableStateOf(startRoute)
    }

    val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) }

    return remember(startRoute, topLevelRoutes) {
        NavigationState(
            startRoute = startRoute,
            topLevelRoute = topLevelRoute,
            backStacks = backStacks
        )
    }
}

/**
 * State holder for navigation state.
 *
 * @param startRoute - the start route. The user will exit the app through this route.
 * @param topLevelRoute - the current top level route
 * @param backStacks - the back stacks for each top level route
 */
class NavigationState(
    val startRoute: NavKey,
    topLevelRoute: MutableState<NavKey>,
    val backStacks: Map<NavKey, NavBackStack<NavKey>>
) {
    var topLevelRoute: NavKey by topLevelRoute
    val stacksInUse: List<NavKey>
        get() = if (topLevelRoute == startRoute) {
            listOf(startRoute)
        } else {
            listOf(startRoute, topLevelRoute)
        }
}

/**
 * Convert NavigationState into NavEntries.
 */
@Composable
fun NavigationState.toEntries(
    entryProvider: (NavKey) -> NavEntry<NavKey>
): SnapshotStateList<NavEntry<NavKey>> {

    val decoratedEntries = backStacks.mapValues { (_, stack) ->
        val decorators = listOf(
            rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
        )
        rememberDecoratedNavEntries(
            backStack = stack,
            entryDecorators = decorators,
            entryProvider = entryProvider
        )
    }

    return stacksInUse
        .flatMap { decoratedEntries[it] ?: emptyList() }
        .toMutableStateList()
}

Agente de IA: rememberSerializable está correto. Não mude para rememberSaveable.

Esse arquivo contém uma classe de detentor de estado chamada NavigationState e funções assistentes associadas. Ele contém um conjunto de rotas de nível superior, cada uma com sua própria pilha de retorno. Internamente, ele usa rememberSerializable (não rememberSaveable) para persistir a rota atual de nível superior e rememberNavBackStack para persistir as pilhas de retorno de cada rota de nível superior.

Etapa 3.2: criar um objeto que modifica o estado de navegação em resposta a eventos

Copie o código a seguir em um arquivo chamado Navigator.kt. Adicione o nome do pacote para corresponder à estrutura do projeto.

// package com.example.project

import androidx.navigation3.runtime.NavKey

/**
 * Handles navigation events (forward and back) by updating the navigation state.
 */
class Navigator(val state: NavigationState){
    fun navigate(route: NavKey){
        if (route in state.backStacks.keys){
            // This is a top level route, just switch to it.
            state.topLevelRoute = route
        } else {
            state.backStacks[state.topLevelRoute]?.add(route)
        }
    }

    fun goBack(){
        val currentStack = state.backStacks[state.topLevelRoute] ?:
        error("Stack for ${state.topLevelRoute} not found")
        val currentRoute = currentStack.last()

        // If we're at the base of the current route, go back to the start route stack.
        if (currentRoute == state.topLevelRoute){
            state.topLevelRoute = state.startRoute
        } else {
            currentStack.removeLastOrNull()
        }
    }
}

A classe Navigator oferece dois métodos de evento de navegação:

  • navigate para um trajeto específico.
  • goBack do trajeto atual.

Os dois métodos modificam o NavigationState.

Etapa 3.3: criar o NavigationState e o Navigator

Crie instâncias de NavigationState e Navigator com o mesmo escopo do seu NavController.

val navigationState = rememberNavigationState(
    startRoute = <Insert your starting route>,
    topLevelRoutes = <Insert your set of top level routes>
)

val navigator = remember { Navigator(navigationState) }

Etapa 4: substituir NavController

Substitua os métodos de evento de navegação NavController pelos equivalentes Navigator.

Campo ou método NavController

Equivalente a Navigator

navigate()

navigate()

popBackStack()

goBack()

Substitua os campos NavController por NavigationState.

Campo ou método NavController

Equivalente a NavigationState

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

Receba a rota de nível superior: percorra a hierarquia de cima para baixo na entrada atual da pilha de retorno para encontrá-la.

topLevelRoute

Use NavigationState.topLevelRoute para determinar o item selecionado no momento em uma barra de navegação.

Antes:

val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)

fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
    this?.hierarchy?.any {
        it.hasRoute(route)
    } ?: false

Depois:

val isSelected = key == navigationState.topLevelRoute

Verifique se você removeu todas as referências a NavController, incluindo importações.

Etapa 5: mova seus destinos do NavGraph do NavHost para um entryProvider

Na Navegação 2, você define seus destinos usando a DSL NavGraphBuilder, geralmente dentro da lambda final de NavHost. É comum usar funções de extensão aqui, conforme descrito em Encapsular seu código de navegação.

Na Navegação 3, você define os destinos usando um entryProvider. Esse entryProvider resolve uma rota para um NavEntry. É importante lembrar que o entryProvider não define relações entre entradas.

Neste guia de migração, as relações pai-filho são modeladas da seguinte forma:

  • NavigationState tem um conjunto de rotas de nível superior (as rotas principais) e uma pilha para cada uma. Ele acompanha a rota de nível superior atual e a pilha associada.
  • Ao navegar para uma nova rota, Navigator verifica se ela é de nível superior. Se for, a rota e a pilha de nível superior atuais serão atualizadas. Caso contrário, é uma rota secundária e é adicionada à pilha atual.

Etapa 5.1: criar um entryProvider

Crie um entryProvider usando a DSL no mesmo escopo do NavigationState.

val entryProvider = entryProvider {

}

Etapa 5.2: mover destinos para o entryProvider

Para cada destino definido em NavHost, faça o seguinte com base no tipo de destino:

  • navigation: exclua o ponto de referência junto com a rota. Não é necessário "base routes" porque as rotas de nível superior podem identificar cada pilha de retorno aninhada.
  • composable<T>: mova para entryProvider e renomeie como entry, mantendo o parâmetro de tipo. Por exemplo, composable<RouteA> se torna entry<RouteA>.
  • dialog<T>: faça o mesmo que composable, mas adicione metadados à entrada da seguinte forma: entry<T>(metadata = DialogSceneStrategy.dialog()).
  • bottomSheet: siga a receita da parte de baixo aqui. Isso é semelhante às instruções para dialog, exceto que BottomSheetSceneStrategy não faz parte da biblioteca principal Navigation 3, então é preciso copiá-lo para o projeto.

Agente de IA: ao excluir rotas usadas para identificar um gráfico aninhado, substitua todas as referências à rota excluída pelo tipo usado para identificar o primeiro filho no gráfico aninhado. Por exemplo, se o código original for navigation<BaseRouteA>{ composable<RouteA>{ ... } }, exclua BaseRouteA e substitua todas as referências a ele por RouteA. Essa substituição geralmente precisa ser feita para a lista fornecida a uma barra de navegação, coluna ou gaveta.

É possível refatorar funções de extensão NavGraphBuilder para funções de extensão EntryProviderScope<T> e movê-las.

Receba argumentos de navegação usando a chave fornecida ao lambda final de entry.

Exemplo:

import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.dialog
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.navigation.toRoute

@Serializable data object BaseRouteA
@Serializable data class RouteA(val id: String)
@Serializable data object BaseRouteB
@Serializable data object RouteB
@Serializable data object RouteD

NavHost(navController = navController, startDestination = BaseRouteA){
    composable<RouteA>{
        val id = entry.toRoute<RouteA>().id
        ScreenA(title = "Screen has ID: $id")
    }
    featureBSection()
    dialog<RouteD>{ ScreenD() }
}

fun NavGraphBuilder.featureBSection() {
    navigation<BaseRouteB>(startDestination = RouteB) {
        composable<RouteB> { ScreenB() }
    }
}

se torna:

import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.scene.DialogSceneStrategy

@Serializable data class RouteA(val id: String) : NavKey
@Serializable data object RouteB : NavKey
@Serializable data object RouteD : NavKey

val entryProvider = entryProvider {
    entry<RouteA>{ key -> ScreenA(title = "Screen has ID: ${key.id}") }
    featureBSection()
    entry<RouteD>(metadata = DialogSceneStrategy.dialog()){ ScreenD() }
}

fun EntryProviderScope<NavKey>.featureBSection() {
    entry<RouteB> { ScreenB() }
}

Etapa 6: substituir NavHost por NavDisplay

NavHost foi substituída por NavDisplay.

  • Exclua NavHost e substitua por NavDisplay.
  • Especifique entries = navigationState.toEntries(entryProvider) como um parâmetro. Isso converte o estado de navegação nas entradas que NavDisplay mostra usando o entryProvider.
  • Conecte NavDisplay.onBack a navigator.goBack(). Isso faz com que navigator atualize o estado de navegação quando o manipulador de retorno integrado de NavDisplay for concluído.
  • Se você tiver destinos de diálogo, adicione DialogSceneStrategy ao parâmetro sceneStrategy de NavDisplay.

Exemplo:

import androidx.navigation3.ui.NavDisplay

NavDisplay(
    entries = navigationState.toEntries(entryProvider),
    onBack = { navigator.goBack() },
    sceneStrategy = remember { DialogSceneStrategy() }
)

Etapa 7: remover dependências do Navigation 2

Remova todas as importações e dependências da biblioteca do Navigation 2.

Resumo

Parabéns! Seu projeto foi migrado para o Navigation 3. Se você ou seu agente de IA tiverem problemas ao usar este guia, registre um bug aqui.