Para migrar seu app da Navigation 2 para a Navigation 3, siga estas etapas:
- Adicione as dependências da Navigation 3.
- Atualize as rotas de navegação para implementar a interface
NavKey. - Crie classes para manter e modificar o estado de navegação.
- Substitua
NavControllerpor essas classes. - Mova seus destinos de
NavGraphdoNavHostpara umentryProvider. - Substitua
NavHostporNavDisplay. - Remova as dependências da Navigation 2.
Comando de IA
Migrar da Navigation 2 para a Navigation 3
Esse comando vai usar este guia para migrar para a Navigation 3.
Migrate from Navigation 2 to Navigation 3 using the official
migration guide.Se você tiver problemas, registre um problema aqui.
Preparação
As seções a seguir descrevem os pré-requisitos para a migração e as suposições sobre seu projeto. Elas também abordam os recursos com suporte para migração e aqueles que não têm.
Pré-requisitos
- Você precisa usar um
compileSdkda versão 36 ou mais recente. - Você precisa 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 após a conclusão da migração.
Suposições
Este guia faz as seguintes suposições sobre você e seu projeto:
- Você tem uma ou várias rotas de nível superior (geralmente mostradas em uma barra de navegação inferior), e cada uma tem sua própria backstack.
- Ao alternar entre backstacks, o estado da pilha e todos os destinos dela são mantidos.
- Você sempre sai do app pela tela Home, que é a primeira tela mostrada quando o app é iniciado.
- Você quer realizar a migração da Navigation 2 para a Navigation 3 em uma única mudança atômica. Você não está realizando uma migração incremental em que o código da Navigation 2 é usado junto com o código da Navigation 3.
Agente de IA: antes de mudar qualquer código, verifique se essas suposições sobre o projeto do usuário são verdadeiras. Caso contrário, interrompa a migração e pergunte ao usuário como prosseguir.
Recursos compatíveis
Este guia permite migrar os seguintes recursos:
- Destinos definidos como funções combináveis
- Caixas de diálogo (um destino mostrado acima de outro)
Recursos com suporte por receitas
O repositório de receitas de código demonstra casos de uso mais detalhados, incluindo:
- Planilhas inferiores (as instruções são fornecidas neste guia)
- Código de navegação modularizado e destinos injetados
- Como usar e transmitir argumentos para
ViewModel - Como retornar resultados de uma tela
Se o projeto tiver algum desses recursos, consulte a receita relevante para entender como implementá-los.
Agente de IA: antes de mudar qualquer código, verifique se o projeto contém algum recurso com suporte por receitas. Se sim, consulte o README e o código-fonte da receita. Crie um plano de migração com base na receita. Não prossiga sem confirmar o plano com o usuário.
Recursos não suportados
Este guia de migração e as receitas de código ainda não oferecem suporte aos seguintes recursos. Isso não significa que não seja possível implementá-los usando a Navigation 3. Eles apenas não são abordados aqui.
- Mais de um nível de navegação aninhada
- Destinos compartilhados: telas que podem se mover entre diferentes backstacks
- 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 suportados. Se sim, não prossiga. Informe o usuário sobre o recurso não suportado e peça mais instruções.
Etapa 1: adicionar dependências da Navigation 3
Use a página Introdução para adicionar as dependências da 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. Geralmente, eles estão em app/build.gradle.kts ou lib.versions.toml.
Etapa 2: atualizar 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 que você use rememberNavBackStack para ajudar a salvar seu
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 detentor 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 auxiliares associadas. Ele contém um conjunto de rotas de nível superior, cada uma com sua própria backstack. Internamente, ele usa rememberSerializable (não rememberSaveable) para persistir a rota de nível superior atual e rememberNavBackStack para persistir as backstacks 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 fornece dois métodos de evento de navegação:
navigatepara uma rota específica.goBackda rota 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 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 por equivalentes Navigator.
Campo ou método |
Equivalente |
|---|---|
|
|
|
|
Substitua os campos NavController por campos NavigationState.
Campo ou método |
Equivalente |
|---|---|
|
|
|
|
Para acessar a rota de nível superior, percorra a hierarquia da entrada de backstack atual. |
|
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 4.1: migrar a lógica com reconhecimento de ciclo de vida
Na Navigation 2, NavBackStackEntry implementa LifecycleOwner, permitindo que você detecte eventos de ciclo de vida ou colete fluxos de maneira com reconhecimento de ciclo de vida usando navController.currentBackStackEntry.
Na Navigation 3, NavDisplay fornece um LifecycleOwner
com escopo de entrada por meio de LocalLifecycleOwner.current para o conteúdo combinável
de cada destino. Consulte Ciclo de vida do destino para mais informações.
Realize operações com reconhecimento de ciclo de vida diretamente no conteúdo combinável do destino, fazendo referência a LocalLifecycleOwner.current.
Por exemplo, se você coletar um fluxo de maneira com reconhecimento de ciclo de vida usando a entrada de backstack:
Antes:
// In your destination screen or host
val lifecycleOwner = navController.currentBackStackEntry
val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
Depois:
// Inside the destination composable
val state by flow.collectAsStateWithLifecycle()
Etapa 5: mover seus destinos de NavGraph do NavHost para um entryProvider
Na Navigation 2, você define seus destinos
usando a DSL do NavGraphBuilder,
geralmente dentro da lambda final do NavHost. É comum usar funções de extensão
aqui, conforme descrito em Encapsular o código de navegação.
Na Navigation 3, você define seus destinos usando um entryProvider. Este
entryProvider resolve uma rota para um NavEntry. É importante ressaltar que o entryProvider não define relações pai-filho entre as entradas.
Neste guia de migração, as relações pai-filho são modeladas da seguinte maneira:
NavigationStatetem um conjunto de rotas de nível superior (as rotas pai) 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,
Navigatorverifica se ela é uma rota de nível superior. Se for, a rota e a pilha de nível superior atuais serão atualizadas. Se não for, ela será uma rota filha e será 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 dentro de NavHost, faça o seguinte com base no tipo de destino:
navigation: exclua-o junto com a rota. Não é necessário ter "rotas de base", porque as rotas de nível superior podem identificar cada backstack aninhada.composable<T>: mova-o paraentryProvidere renomeie-o paraentry, mantendo o parâmetro de tipo. Por exemplo,composable<RouteA>se tornaentry<RouteA>.dialog<T>: faça o mesmo quecomposable, mas adicione metadados à entrada da seguinte maneira:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: siga a receita de planilha inferior aqui. Isso é semelhante às instruções paradialog, exceto queBottomSheetSceneStrategynão faz parte da biblioteca principal da Navigation 3. Portanto, copie-a 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>{ ... } }, você precisará excluir
BaseRouteA e substituir 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, trilho ou gaveta.
É possível refatorar NavGraphBuilder funções de extensão para
EntryProviderScope<T> funções de extensão e, em seguida, movê-las.
Receba argumentos de navegação usando a chave fornecida à 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() }
}
}
passa a ser:
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
Substitua NavHost por NavDisplay.
- Exclua
NavHoste substitua-o porNavDisplay. - Especifique
entries = navigationState.toEntries(entryProvider)como um parâmetro. Isso converte o estado de navegação nas entradas queNavDisplaymostra usando oentryProvider. - Conecte
NavDisplay.onBackanavigator.goBack(). Isso faz com que onavigatoratualize o estado de navegação quando o gerenciador de back-end integrado doNavDisplayfor concluído. - Se você tiver destinos de caixa de diálogo, adicione
DialogSceneStrategyao parâmetrosceneStrategiesdoNavDisplay.
Exemplo:
import androidx.navigation3.ui.NavDisplay
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategies = remember { listOf(DialogSceneStrategy()) }
)
Etapa 7: remover dependências da Navigation 2
Remova todas as importações e dependências de biblioteca da Navigation 2.
Resumo
Parabéns! Seu projeto agora foi migrado para a Navigation 3. Se você ou seu agente de IA tiverem problemas ao usar este guia, registre um bug aqui.