Segurança de tipo na DSL do Kotlin e no Navigation Compose

Esta página contém práticas de segurança recomendadas no ambiente de execução para a DSL do Kotlin de Navigation e no Navigation Compose. Em resumo, você precisa mapear cada tela do app ou gráfico de navegação para um arquivo de navegação por módulo. Os arquivos resultantes precisam conter todas as informações relacionadas à navegação para o destino especificado. É também nesses arquivos de navegação que os modificadores de visibilidade do Kotlin oferecem segurança de tipo para o ambiente de execução:

  • As funções seguras são expostas publicamente ao restante da base de código.
  • Os conceitos de navegação para uma tela ou gráfico de navegação específicos são colocalizados e permanecem particulares no mesmo arquivo para que fiquem inacessíveis ao restante da base de código.

Dividir o gráfico de navegação

Divida seu gráfico de navegação por tela. Essa é praticamente a mesma abordagem usada nas divisões de tela para diferentes função de composição. Cada tela precisa ter uma função de extensão NavGraphBuilder.

Essa função de extensão é a ponte entre uma função de composição sem estado e a lógica específica da navegação. Essa camada também pode definir de onde o estado vem e como os eventos são processados.

Veja uma ConversationScreen típica que pode ser internal ao próprio módulo, para que outros não possam acessá-lo:

// ConversationScreen.kt

@Composable
internal fun ConversationScreen(
  uiState: ConversationUiState,
  onPinConversation: () -> Unit,
  onNavigateToParticipantList: (conversationId: String) -> Unit
) { ... }

A função de extensão NavGraphBuilder a seguir adiciona o elemento ConversationScreen combinável como um destino desse NavGraph. Ela também conecta a tela a um ViewModel que fornece o estado da IU e processa a lógica de negócios relacionada à tela. Eventos de navegação que não podem ser processados pelo ViewModel são expostos ao autor da chamada.

// ConversationNavigation.kt

private const val conversationIdArg = "conversationId"

// Adds conversation screen to `this` NavGraphBuilder
fun NavGraphBuilder.conversationScreen(
  // Navigation events are exposed to the caller to be handled at a higher level
  onNavigateToParticipantList: (conversationId: String) -> Unit
) {
  composable("conversation/{$conversationIdArg}") {
    // The ViewModel as a screen level state holder produces the screen
    // UI state and handles business logic for the ConversationScreen
    val viewModel: ConversationViewModel = hiltViewModel()
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    ConversationScreen(
      uiState,
      ::viewModel.pinConversation,
      onNavigateToParticipantList
    )
  }
}

O arquivo ConversationNavigation.kt separa o código da biblioteca Navigation do próprio destino. Ele também oferece encapsulamento de conceitos de navegação, como rotas ou IDs de argumentos mantidos particulares, porque não podem ser vazados para fora desse arquivo. Os eventos de navegação que não podem ser processados nessa camada precisam ser expostos ao autor da chamada para que sejam tratados no nível correto. Você vai encontrar um exemplo desse evento com onNavigateToParticipantList no snippet de código acima.

Navegação com segurança de tipo

A DSL de navegação do Kotlin em que o Navigation Compose é criado não oferece atualmente segurança de tipo no tempo de compilação da forma como Safe Args oferece para gráficos de navegação criados em arquivos de navegação com recurso XML. O Safe Args gera um código que contém classes e métodos com segurança de tipo para destinos e ações de navegação. No entanto, você pode estruturar o código de navegação para que ele tenha segurança de tipo no momento da execução. Assim, você pode evitar falhas e garantir que:

  • Os argumentos fornecidos ao navegar para um gráfico de destino ou navegação sejam dos tipos certos e que todos os argumentos necessários estejam presentes.
  • Os argumentos recuperados de SavedStateHandle sejam dos tipos corretos.

Cada destino também precisa expor uma função de extensão NavController para que outros destinos possam navegar com segurança até ele.

// ConversationNavigation.kt

fun NavController.navigateToConversation(conversationId: String) {
    this.navigate("conversation/$conversationId")
}

Se você quiser navegar até uma tela específica do app com diferentes NavOptions, como popUpTo, savedState, restoreState ou singleTop, transmita um parâmetro opcional à função de extensão NavController.

// HomeNavigation.kt

const val HomeRoute = "home"

fun NavController.navigateToHome(navOptions: NavOptions? = null) {
    this.navigate(HomeRoute, navOptions)
}

Wrapper de argumentos com segurança de tipo

Você também pode criar um wrapper com segurança de tipo para extrair os argumentos de um SavedStateHandle para o ViewModel e de uma NavBackStackEntry no conteúdo de um destino para ter os benefícios mencionados na introdução desta seção.

// ConversationNavigation.kt

private const val conversationIdArg = "conversationId"

internal class ConversationArgs(val conversationId: String) {
  constructor(savedStateHandle: SavedStateHandle) :
    this(checkNotNull(savedStateHandle[conversationIdArg]) as String)
}

// ConversationViewModel.kt

internal class ConversationViewModel(...,
  savedStateHandle: SavedStateHandle
) : ViewModel() {
  private val conversationArgs = ConversationArgs(savedStateHandle)
}

Criar o gráfico de navegação

Os gráficos de navegação usam as funções de extensão com segurança de tipo descritas acima para adicionar destinos e navegar até eles.

No exemplo a seguir, o destino conversation junto com dois outros, home e participant list, é incluído no NavHost no nível do app da seguinte maneira:

// MyApp.kt

@Composable
fun MyApp(modifier: Modifier = Modifier) {
  val navController = rememberNavController()
  NavHost(
    navController = navController,
    startDestination = HomeRoute,
    modifier = modifier
  ) {

    homeScreen(
      onNavigateToConversation = { conversationId ->
        navController.navigateToConversation(conversationId)
      }
    )

    conversationScreen(
      onNavigateToParticipantList = { conversationId ->
        navController.navigateToParticipantList(conversationId)
      }
    }

    participantListScreen()
}

Segurança de tipo em gráficos de navegação aninhados

Escolha a visibilidade correta para módulos que fornecem várias telas. Esse é o mesmo conceito usado para os métodos nas seções acima. No entanto, pode não fazer sentido expor telas individuais a outros módulos. Nesse caso, elas devem ser tratadas como parte de um fluxo maior e autossuficiente.

Esse conjunto de telas autônomo é chamado de gráfico de navegação aninhado. Ele permite incluir várias telas em um único método de extensão NavGraphBuilder. Esse método usa os métodos de extensão NavController para vincular as telas no mesmo módulo.

No exemplo a seguir, o destino conversation descrito nas seções anteriores aparece em um gráfico de navegação aninhado ao lado de dois outros destinos, conversation list e participant list:

// ConversationGraphNavigation.kt

private val ConversationGraphRoutePattern = "conversation"

fun NavController.navigateToConversationGraph(navOptions: NavOptions? = null) {
  this.navigate(ConversationGraphRoutePattern, navOptions)
}

fun NavGraphBuilder.conversationGraph(navController: NavController) {
  navigation(
    startDestination = ConversationListRoutePattern,
    route = ConversationGraphRoutePattern
  ) {
    conversationListScreen(
      onNavigateToConversation = { conversationId ->
        navController.navigateToConversation(conversationId)
      }
    )
    conversationScreen(
      onNavigateToParticipantList = { conversationId ->
        navController.navigateToParticipantList(conversationId)
      }
    )
    partipantList()
}

Você pode usar vários gráficos de navegação aninhados no NavHost no nível do app desta maneira:

// MyApp.kt

@Composable
fun MyApp(modifier: Modifier = Modifier) {
  val navController = rememberNavController()
  NavHost(
    navController = navController,
    startDestination = HomeGraphRoutePattern
    modifier = modifier
  ) {
    homeGraph(
      navController,
      onNavigateToConversation = {
        navController.navigateToConversationGraph()
      }
    }
    conversationGraph(navController)
}