Cómo encapsular tu código de navegación

Cuando usas el DSL de Kotlin para construir tu gráfico, puede ser difícil mantener los destinos y los eventos de navegación en un solo archivo. Esto es especialmente cierto si tienes varios atributos independientes.

Extrae destinos

Debes mover tus destinos a las funciones de extensión NavGraphBuilder. Deben vivir cerca de las rutas que los definen y de las pantallas que muestran. Por ejemplo, considera el siguiente código a nivel de la app que crea un destino que muestra una lista de contactos:

// MyApp.kt

@Serializable
object Contacts

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

Debes mover el código específico de la navegación a un archivo separado:

// ContactsNavigation.kt

@Serializable
object Contacts

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

// MyApp.kt

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

Las definiciones de rutas y destino ahora están separadas de la app principal y puedes actualizarlas de forma independiente. La app principal solo depende de una sola función de extensión. En este caso, es NavGraphBuilder.contactsDestination().

La función de extensión NavGraphBuilder forma el puente entre una función de componibilidad sin estado en el nivel de la pantalla y una lógica específica de Navigation. Esta capa también puede definir de dónde proviene el estado y cómo controlas los eventos.

Ejemplo

En el siguiente fragmento, se presenta un destino nuevo para mostrar los detalles de un contacto y se actualiza el destino existente de la lista de contactos a fin de exponer un evento de navegación para mostrar los detalles del contacto.

Este es un conjunto típico de pantallas que pueden ser internal en su propio módulo, de modo que otros módulos no puedan acceder a ellas:

// 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) { ... }

Cómo crear destinos

La siguiente función de extensión NavGraphBuilder crea un destino que muestra el elemento ConversationScreen componible. Además, ahora conecta la pantalla con un ViewModel que proporciona el estado de la IU de la pantalla y controla su lógica empresarial relacionada.

Los eventos de navegación, como la navegación al destino de los detalles del contacto, se exponen al emisor en lugar de ser manejados por 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
    )
  }
}

Puedes usar el mismo enfoque para crear un destino que muestre ContactDetailsScreen. En este caso, en lugar de obtener el estado de la IU a partir de un modelo de vista, puedes obtenerlo directamente de NavBackStackEntry.

// ContactsNavigation.kt

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

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

Cómo encapsular eventos de navegación

De la misma manera que encapsulas destinos, puedes encapsular eventos de navegación para evitar exponer tipos de rutas innecesariamente. Para ello, crea funciones de extensión en NavController.

// ContactsNavigation.kt

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

Integración

Ahora, el código de navegación para mostrar contactos está bien separado del gráfico de navegación de la app. La app debe hacer lo siguiente:

  • Llama a funciones de extensión NavGraphBuilder para crear destinos
  • Conecta esos destinos llamando a las funciones de extensión NavController para los eventos de navegación.
// MyApp.kt

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

Resumen

  • Para encapsular tu código de navegación para un conjunto de pantallas relacionado, colócalo en un archivo separado.
  • Crea funciones de extensión en NavGraphBuilder para exponer los destinos.
  • Crea funciones de extensión en NavController para exponer eventos de navegación.
  • Usa internal para mantener la privacidad de las pantallas y los tipos de rutas