Habilidades do Android
Ver no GitHubNavegação do Jetpack 3
android skills add navigation-3Para migrar seu app do Navigation 2 para o Navigation 3, siga estas etapas:
- Adicione as dependências do Navigation 3.
- Atualize suas rotas de navegação para implementar a interface
NavKey. - Crie classes para armazenar e modificar o estado de navegação.
- Substitua
NavControllerpor essas classes. - Mova seus destinos do
NavGraphdeNavHostpara umentryProvider. - Substitua
NavHostporNavDisplay. - Remova as dependências do Navigation 2.
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
compileSdk36 ou mais recente. - É necessário conhecer a terminologia de navegação.
- Os destinos são funções combináveis. A Navigation 3 foi criada 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 uma ou várias rotas de nível superior (geralmente mostradas em uma barra de navegação na parte de baixo), e cada uma tem a própria backstack.
- 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 é iniciado.
- 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 essas 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:
- Páginas inferiores (instruções neste guia)
- Código de navegação modularizado e destinos injetados
- Como usar e transmitir argumentos para
ViewModel - Retornar resultados de uma tela
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, confira o LEIAME 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 são compatíveis com os seguintes recursos. Isso não significa que você não possa implementá-los usando o 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 pilhas de retorno
- Tipos de destino personalizados
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.1.7"
# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.11.0"
[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 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
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:
navigatepara um trajeto específico.goBackdo trajeto atual.
Os dois métodos modificam o NavigationState.
Etapa 3.3: criar os NavigationState e 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 |
Equivalente a |
|---|---|
|
|
|
|
|
|
Substitua os campos NavController por NavigationState.
Campo ou método |
Equivalente a |
|---|---|
|
|
|
|
|
|
|
|
Receba a rota de nível superior: percorra a hierarquia da entrada atual da pilha de retorno para encontrá-la. |
|
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 Navegação 2, NavBackStackEntry implementa LifecycleOwner, permitindo que você
detecte eventos de ciclo de vida ou colete fluxos de maneira compatível com o ciclo de vida usando
navController.currentBackStackEntry.
Na Navigation 3, o NavDisplay fornece um LifecycleOwner com escopo de entrada
pelo 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 referenciando LocalLifecycleOwner.current.
Por exemplo, se você coletar um fluxo de maneira ciente do ciclo de vida usando a entrada da 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 4.2: migrar a transmissão de resultados
Na Navegação 2, os destinos transmitiam resultados de volta para destinos anteriores usando
o SavedStateHandle em NavBackStackEntry. Como SavedStateHandle é compatível com o estado de instância salvo, os dados retornados sobreviveram ao encerramento do processo automaticamente.
Antes:
// Sender destination: navController.previousBackStackEntry?.savedStateHandle?.set("contact_key", contact) navController.popBackStack() // Receiver destination: val lifecycleOwner = LocalLifecycleOwner.current navController.currentBackStackEntry?.savedStateHandle ?.getLiveData<Contact>("contact_key") ?.observe(lifecycleOwner) { contact -> viewModel.onRecipientSelected(contact) }
No Navigation 3, adicione
rememberResultEventBusNavEntryDecorator() ao
seu NavDisplay.entryDecorators. No mapeamento entryProvider do destino do remetente, recupere LocalResultEventBus.current e chame sendResult(). No destino de recebimento, use
ResultEffect para encaminhar o evento a um ViewModel ou
acionar um efeito colateral.
Depois:
// Sender destination (in entryProvider): entry<ContactPickerRoute> { val resultBus = LocalResultEventBus.current ContactPickerScreen( onContactSelected = { contact -> resultBus.sendResult<Contact>(result = contact) navigator.goBack() } ) } // Receiver destination: @Composable fun ComposeMessageScreen(viewModel: ComposeMessageViewModel = viewModel()) { ResultEffect<Contact> { contact -> viewModel.onRecipientSelected(contact) } ComposeMessageContent(recipient = viewModel.recipient) }
Para observação baseada em estado, chame resultBus.conflateAsState(). Para
mais detalhes, consulte Retornar resultados.
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 pai-filho entre entradas.
Neste guia de migração, as relações pai-filho são modeladas da seguinte forma:
NavigationStatetem um conjunto de rotas de nível superior (as rotas principais) e uma pilha para cada uma delas. Ele acompanha a rota de nível superior atual e a pilha associada.- Ao navegar para uma nova rota,
Navigatorverifica 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<NavKey> { }
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 junto com a rota. Não é necessário "base routes" porque as rotas de nível superior podem identificar cada backstack aninhada.composable<T>: mova paraentryProvidere renomeie comoentry, 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 forma:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: siga a receita da página inferior aqui. Isso é semelhante às instruções paradialog, exceto queBottomSheetSceneStrategynão faz parte da biblioteca principal Navigation 3, então você precisa copiá-la para seu 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, um trilho ou
uma 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 @Composable fun NavHostSnippet(navController: NavHostController) { NavHost(navController = navController, startDestination = BaseRouteA){ composable<RouteA>{ entry -> 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: substitua NavHost por NavDisplay
Substitua NavHost por NavDisplay.
- Exclua
NavHoste substitua porNavDisplay. - Especifique
entries = navigationState.toEntries(entryProvider)como um parâmetro. Isso converte o estado de navegação nas entradas queNavDisplaymostra usando oentryProvider. - Conectar
NavDisplay.onBackanavigator.goBack(). Isso faz com quenavigatoratualize o estado de navegação quando o gerenciador de retorno integrado deNavDisplayfor concluído. - Se você tiver destinos de diálogo, adicione
DialogSceneStrategyao parâmetrosceneStrategiesdeNavDisplay.
Exemplo:
NavDisplay( entries = navigationState.toEntries(entryProvider), onBack = { navigator.goBack() }, sceneStrategies = remember { listOf(DialogSceneStrategy()) } )
Etapa 7: migrar links diretos
Na Navegação 2, os links diretos eram definidos diretamente no gráfico de navegação
usando o parâmetro deepLinks dos destinos.
Na Navegação 3, os links diretos são gerenciados de forma independente da interface de navegação. Você
define DeepLinkMatchers e corresponde às solicitações recebidas na sua Activity para
construir a backstack inicial.
Antes:
Na Navegação 2, você pode ter definido um link direto assim:
composable<RouteA>( deepLinks = listOf( navDeepLink { uriPattern = "www.example.com/user/{id}" } ) ) { // ... }
Depois:
Na Navegação 3, você define um UriDeepLinkMatcher para a rota:
val userMatcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/user/{id}"), serializer<RouteA>() )
Em seguida, na onCreate (e onNewIntent) da sua atividade, corresponda à intent
recebida e inicialize a backstack:
val deepLinkMatchers: List<DeepLinkMatcher<*, *>> = listOf( userMatcher, ) class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val request = DeepLinkRequest(intent = intent) val matchResult = deepLinkMatchers .mapNotNull { it.match(request) } .maxOrNull() val backStack = when (matchResult) { null -> listOf(HomeKey) is BackStackMatchResult<*, *> -> { @Suppress("UNCHECKED_CAST") matchResult.backStack as List<NavKey> } else -> listOf(matchResult.key) } // Use backStack with NavDisplay } }
Tipos de argumentos personalizados
Na Navegação 2, você processou tipos de argumentos personalizados ou de terceiros (como
LocalDateTime) usando implementações NavType personalizadas e typeMap.
Na navegação 3, defina um DeepLinkSerializer para desserializar tipos personalizados ou
de terceiros de parâmetros de URI. Consulte Serialização personalizada com DeepLinkSerializer para mais detalhes.
Para casos de uso mais avançados, incluindo pilhas de retorno sintéticas e correspondentes personalizados, consulte o guia Suporte a links diretos.
Etapa 8: remover dependências do Navigation 2
Remova todas as importações do Navigation 2 e as dependências da biblioteca.
Resumo
Parabéns! Seu projeto agora foi migrado para o Navigation 3. Se você ou seu agente de IA tiverem problemas ao usar este guia, registre um bug aqui.