Encapsular seu código de navegação

Ao usar a DSL do Kotlin para criar um gráfico, pode ser difícil manter destinos e eventos de navegação em um único arquivo. Isso é especialmente verdadeiro se você tem vários recursos independentes.

Extrair destinos

Mova seus destinos para as funções de extensão NavGraphBuilder. Eles precisam estar perto dos trajetos que os definem e das telas que exibem. Por exemplo, considere o código abaixo no nível do app que cria um destino que mostra uma lista de contatos:

// MyApp.kt

@Serializable
object Contacts

@Composable
fun MyApp() {
  ...
  NavHost(navController, startDestination = Contacts) {
    composable<Contacts> { ContactsScreen( /* ... */ ) }
  }
}

Você precisa mover o código específico de navegação para um arquivo separado:

// ContactsNavigation.kt

@Serializable
object Contacts

fun NavGraphBuilder.contactsDestination() {
    composable<Contacts> { ContactsScreen( /* ... */ ) }
}

// MyApp.kt

@Composable
fun MyApp() {
  ...
  NavHost(navController, startDestination = Contacts) {
     contactsDestination()
  }
}

As definições de rotas e destino agora estão separadas do app principal e podem ser atualizadas de forma independente. O app principal só depende de uma única função de extensão. Nesse caso, é NavGraphBuilder.contactsDestination().

A função de extensão NavGraphBuilder forma a ponte entre uma função combinável sem estado da tela e a lógica específica da navegação. Essa camada também pode definir de onde vem o estado e como você gerencia os eventos.

Exemplo

O snippet a seguir introduz um novo destino para mostrar os detalhes de um contato e atualiza o destino da lista de contatos existente para expor um evento de navegação para mostrar os detalhes do contato.

Confira um conjunto típico de telas que podem ser internal no próprio módulo, para que outros módulos não possam acessá-las:

// ContactScreens.kt

// Displays a list of contacts
@Composable
internal fun ContactsScreen(
  uiState: ContactsUiState,
  onNavigateToContactDetails: (contactId: String) -> Unit
) { ... }

// Displays the details for an individual contact
@Composable
internal fun ContactDetailsScreen(contact: ContactDetails) { ... }

Criar destinos

A função de extensão NavGraphBuilder a seguir cria um destino que mostra o elemento combinável ConversationScreen. Além disso, agora ela conecta a tela a um ViewModel que fornece o estado da interface da tela e processa a lógica de negócios relacionada à tela.

Eventos de navegação, como navegar até o destino dos detalhes do contato, são expostos ao autor da chamada em vez de serem processados pelo ViewModel.

// ContactsNavigation.kt

@Serializable
object Contacts

// Adds contacts destination to `this` NavGraphBuilder
fun NavGraphBuilder.contactsDestination(
  // Navigation events are exposed to the caller to be handled at a higher level
  onNavigateToContactDetails: (contactId: String) -> Unit
) {
  composable<Contacts> {
    // The ViewModel as a screen level state holder produces the screen
    // UI state and handles business logic for the ConversationScreen
    val viewModel: ContactsViewModel = hiltViewModel()
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    ContactsScreen(
      uiState,
      onNavigateToContactDetails
    )
  }
}

Você pode usar a mesma abordagem para criar um destino que mostre o ContactDetailsScreen. Nesse caso, em vez de conferir o estado da interface de um modelo de visualização, é possível fazer isso diretamente do NavBackStackEntry.

// ContactsNavigation.kt

@Serializable
internal data class ContactDetails(val id: String)

fun NavGraphBuilder.contactDetailsScreen() {
  composable<ContactDetails> { navBackStackEntry ->
    ContactDetailsScreen(contact = navBackStackEntry.toRoute())
  }
}

Encapsular eventos de navegação

Da mesma forma que você encapsula destinos, é possível encapsular eventos de navegação para evitar a exposição desnecessária de tipos de trajeto. Para fazer isso, crie funções de extensão no NavController.

// ContactsNavigation.kt

fun NavController.navigateToContactDetails(id: String) {
  navigate(route = ContactDetails(id = id))
}

Integre

O código de navegação para mostrar contatos agora está limpo do gráfico de navegação do app. O app precisa:

  • Chamar funções de extensão NavGraphBuilder para criar destinos
  • Conecte esses destinos chamando funções de extensão NavController para eventos de navegação.
// MyApp.kt

@Composable
fun MyApp() {
  ...
  NavHost(navController, startDestination = Contacts) {
     contactsDestination(onNavigateToContactDetails = { contactId ->
        navController.navigateToContactDetails(id = contactId)
     })
     contactDetailsDestination()
  }
}

Em resumo

  • Encapsular seu código de navegação para um conjunto de telas relacionadas colocando-o em um arquivo separado
  • Exponha destinos criando funções de extensão em NavGraphBuilder.
  • Exponha eventos de navegação criando funções de extensão no NavController.
  • Use internal para manter a privacidade das telas e dos tipos de rota