Guia: como migrar para a navegação com segurança de tipos no Compose e na Navigation 2

Este guia descreve o processo de substituição de rotas baseadas em strings por tipos Kotlin serializáveis para alcançar segurança no tempo de compilação e eliminar falhas no tempo de execução causadas por erros de digitação ou tipos de argumentos incorretos.

Pré-requisitos

Antes de iniciar a migração, verifique se o projeto atende aos seguintes requisitos:

  1. Versão do Navigation: atualize para o Jetpack Navigation 2.8.0 ou mais recente.
  2. Plug-in de serialização do Kotlin:
  3. Adicione o plug-in a libs.versions.toml:
[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }

[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
  • Adicione as dependências aos arquivos build.gradle.kts de nível superior e do módulo.build.gradle.kts

Etapa 1: definir seus destinos

Substitua as strings de rotas constantes por objetos e classes @Serializable.

  • Para telas sem argumentos: use um data object
  • Para telas com argumentos: use um data class

Antes (com base em string):

const val ROUTE_HOME = "home"
const val ROUTE_PROFILE = "profile/{userId}"

Depois (com segurança de tipo):

import kotlinx.serialization.Serializable

@Serializable
object Home

@Serializable
data class Profile(val userId: String)

Etapa 2: atualizar a configuração do NavHost

Atualize seu NavHost para usar os novos tipos genéricos na função composable e dialog.

Antes:

NavHost(navController, startDestination = "home") {
    composable("home") { HomeScreen(...) }
    composable("profile/{userId}") { backStackEntry ->
        val userId = backStackEntry.arguments?.getString("userId")
        ProfileScreen(userId)
    }
}

Depois:

NavHost(navController, startDestination = Home) {
    composable<Home> {
        HomeScreen(...)
    }
    composable<Profile> { backStackEntry ->
        // The library automatically handles argument extraction
        val profile: Profile = backStackEntry.toRoute()
        ProfileScreen(profile.userId)
    }
}

Etapa 3: implementar chamadas de navegação com segurança de tipo

Substitua as chamadas de navegação com interpolação de strings por instâncias de classe.

Antes:

navController.navigate("profile/user123")

Depois:

navController.navigate(Profile(userId = "user123"))

Etapa 4: acessar argumentos em ViewModels

Se você usa um ViewModel, agora é possível extrair o objeto de rota diretamente do SavedStateHandle.

Implementação:

class ProfileViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    // Automatically parses arguments into the Profile class
    private val profile = savedStateHandle.toRoute<Profile>()
    val userId = profile.userId
}

Etapa 5: (avançado) processar tipos personalizados

Se você precisar transmitir classes de dados complexas (não apenas primitivas), defina um NavType personalizado.

  1. Crie o tipo personalizado: ```kotlin val SearchFilterType = object : NavType(isNullableAllowed = false) { override fun get(bundle: Bundle, key: String): SearchFilter? = Json.decodeFromString(bundle.getString(key) ?: return null)
override fun parseValue(value: String): SearchFilter =
    Json.decodeFromString(Uri.decode(value))

override fun put(bundle: Bundle, key: String, value: SearchFilter) {
    bundle.putString(key, Json.encodeToString(value))
}

}



2. **Register it in the Graph**:
```kotlin
composable<Search>(
    typeMap = mapOf(typeOf<SearchFilter>() to SearchFilterType)
) { ... }

Práticas recomendadas e dicas

  • Hierarquias seladas: para apps grandes, agrupe suas rotas usando uma interface ou classe selada para manter a estrutura de navegação organizada.
  • Instâncias de objeto: para rotas sem parâmetros, sempre use object em vez de class para evitar alocações desnecessárias.
  • Tipos anuláveis: a nova API é compatível com tipos anuláveis (por exemplo, data class Search(val query: String?)) e fornece valores padrão automaticamente.
  • Teste: use navController.currentBackStackEntry?.hasRoute<T>() para verificar o destino atual de maneira segura durante os testes de UI.