Cómo compilar un gráfico de manera programática con el DSL de Kotlin

El componente Navigation proporciona un lenguaje específico de dominio basado en Kotlin o DSL que se basa en compiladores seguros para tipos de Kotlin. Esta API te permite componer el gráfico de manera declarativa en el código Kotlin, en lugar de tener que hacerlo en un recurso XML. Esto puede resultar útil si deseas compilar la navegación de la app de forma dinámica. Por ejemplo, la app podría descargar y almacenar en caché una configuración de navegación de un servicio web externo y, luego, usarla para compilar de forma dinámica un gráfico de navegación en la función onCreate() de la actividad.

Dependencias

Para usar el DSL de Kotlin, agrega la siguiente dependencia al archivo build.gradle de tu app:

Groovy

dependencies {
    def nav_version = "2.7.7"

    api "androidx.navigation:navigation-fragment-ktx:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.7.7"

    api("androidx.navigation:navigation-fragment-ktx:$nav_version")
}

Cómo compilar un gráfico

Comencemos con un ejemplo básico basado en la app de Sunflower. Para este ejemplo, tenemos dos destinos: home y plant_detail. El destino home está presente cuando el usuario inicia la app por primera vez. Este destino muestra una lista de plantas del jardín del usuario. Cuando el usuario selecciona una de las plantas, la app navega hacia el destino plant_detail.

En la figura 1, se muestran estos destinos junto con los argumentos que requiere el destino plant_detail y una acción, to_plant_detail, que la app usa para navegar del objeto home a plant_detail.

La app de Sunflower tiene dos destinos junto con una acción que los conecta.
Figura 1: La app de Sunflower tiene dos destinos, home y plant_detail, junto con una acción que los conecta.

Cómo alojar un gráfico de navegación DSL de Kotlin

Antes de compilar el gráfico de navegación de tu app, necesitas un lugar para alojarlo. En este ejemplo, se usan fragmentos, por lo que se aloja el gráfico en un NavHostFragment dentro de una FragmentContainerView:

<!-- activity_garden.xml -->
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true" />

</FrameLayout>

Ten en cuenta que el atributo app:navGraph no se establece en este ejemplo. El gráfico no se define como un recurso en la carpeta res/navigation, por lo que debe configurarse como parte del proceso de onCreate() en la actividad.

En XML, una acción vincula un ID de destino con uno o más argumentos. Sin embargo, cuando se usa la DSL de Navigation, una ruta puede contener argumentos como parte de ella. Eso significa que no hay un concepto de acciones cuando se usa la DSL.

El siguiente paso es definir algunas constantes que usarás cuando definas tu gráfico.

Cómo crear constantes para el gráfico

Los gráficos de navegación basados en XML se analizan como parte del proceso de compilación de Android. Se crea una constante numérica para cada atributo id definido en el gráfico. Estos IDs estáticos generados en el tiempo de compilación no están disponibles cuando se compila el gráfico de navegación durante el tiempo de ejecución, por lo que la DSL de Navigation usa cadenas de ruta en lugar de IDs. Cada ruta está representada por una cadena única, y es recomendable definirla como constante para reducir el riesgo de errores relacionados con la escritura.

Los argumentos están integrados en la cadena de ruta. Integrar esta lógica en la ruta puede reducir, una vez más, el riesgo de que se produzcan errores relacionados con la escritura.

object nav_routes {
    const val home = "home"
    const val plant_detail = "plant_detail"
}

object nav_arguments {
    const val plant_id = "plant_id"
    const val plant_name = "plant_name"
}

Una vez que hayas definido las constantes, podrás compilar el gráfico de navegación.

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
    startDestination = nav_routes.home
) {
    fragment<HomeFragment>(nav_routes.home) {
        label = resources.getString(R.string.home_title)
    }

    fragment<PlantDetailFragment>("${nav_routes.plant_detail}/{${nav_arguments.plant_id}}") {
        label = resources.getString(R.string.plant_detail_title)
        argument(nav_arguments.plant_id) {
            type = NavType.StringType
        }
    }
}

En este ejemplo, la expresión lambda final define dos destinos de fragmentos con la función de compilador de DSL fragment(). Esta función requiere una cadena de ruta para el destino que se obtiene de las constantes. La función también acepta una expresión lambda opcional para la configuración adicional, como la etiqueta de destino, además de funciones de compilador incorporadas para argumentos y vínculos directos.

La clase Fragment que administra la IU de cada destino se pasa como un tipo con parámetro dentro de corchetes angulares (<>). Esto tiene el mismo efecto que configurar el atributo android:name en los destinos de fragmentos que se definen con XML.

Por último, puedes navegar de home a plant_detail con llamadas NavController.Navigate() estándar:

private fun navigateToPlant(plantId: String) {
   findNavController().navigate("${nav_routes.plant_detail}/$plantId")
}

En PlantDetailFragment, puedes obtener el valor del argumento como se muestra en el siguiente ejemplo:

val plantId: String? = arguments?.getString(nav_arguments.plant_id)

Puedes encontrar detalles sobre cómo proporcionar argumentos durante la navegación en la sección Cómo proporcionar argumentos de destino.

En el resto de esta guía, se describen los elementos comunes del gráfico de navegación, los destinos y cómo usarlos a la hora de compilar el gráfico.

Destinos

La DSL de Kotlin proporciona compatibilidad integrada con tres tipos de destino: Fragment, Activity y NavGraph, cada uno de los cuales tiene su propia función de extensión intercalada disponible para compilar y configurar el destino.

Destinos de fragmentos

La función DSL fragment() puede parametrizarse para la clase de fragmento de implementación y toma una cadena de ruta única para asignarla a este destino, seguida de una lambda en la que puedes proporcionar una configuración adicional, como se describe en Cómo navegar con tu gráfico DSL de Kotlin.

fragment<FragmentDestination>(nav_routes.route_name) {
   label = getString(R.string.fragment_title)
   // arguments, deepLinks
}

Destino de la actividad

La función DSL activity() toma una cadena de ruta única para asignarla a este destino, pero no se le atribuyen parámetros para ninguna clase de actividad de implementación. En su lugar, configuras un objeto activityClass opcional en una expresión lambda final. Esta flexibilidad te permite definir un destino de actividad para una actividad que se debería iniciar con un intent implícito, en el que una clase de actividad explícita no tendría sentido. Al igual que con los destinos de fragmentos, también puedes configurar una etiqueta, argumentos y vínculos directos.

activity(nav_routes.route_name) {
   label = getString(R.string.activity_title)
   // arguments, deepLinks...

   activityClass = ActivityDestination::class
}

La función DSL navigation() se puede usar para compilar un gráfico de navegación anidado. Esta función utiliza tres argumentos: una ruta para asignar al gráfico, la ruta de destino del gráfico y una lambda para configuraciones adicionales del gráfico. Entre los elementos válidos, se incluyen otros destinos, argumentos, vínculos directos y una etiqueta descriptiva del destino. Esta etiqueta puede ser útil para vincular el gráfico de navegación a componentes de la IU con NavigationUI.

navigation("route_to_this_graph", nav_routes.home) {
   // label, other destinations, deep links
}

Compatibilidad con destinos personalizados

Si usas un tipo de destino nuevo que no admite directamente la DSL de Kotlin, puedes agregar estos destinos a la DSL de Kotlin usando addDestination():

// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
    route = Graph.CustomDestination.route
}
addDestination(customDestination)

Como alternativa, también puedes usar el operador unario más para agregar un destino recién construido directamente al gráfico:

// The NavigatorProvider is retrieved from the NavController
+navigatorProvider[CustomNavigator::class].createDestination().apply {
    route = Graph.CustomDestination.route
}

Cómo proporcionar argumentos de destino

Cualquier destino puede definir los argumentos que son opcionales u obligatorios. Las acciones se pueden definir con la función argument() en NavDestinationBuilder, que es la clase base de todos los tipos de compiladores de destino. Esta función usa el nombre del argumento como cadena y una lambda que se utiliza para construir y configurar un NavArgument.

Dentro de la lambda, puedes especificar el tipo de datos del argumento, un valor predeterminado (cuando corresponda) y si puede anularse o no.

fragment<PlantDetailFragment>("${nav_routes.plant_detail}/{${nav_arguments.plant_id}}") {
    label = getString(R.string.plant_details_title)
    argument(nav_arguments.plant_id) {
        type = NavType.StringType
        defaultValue = getString(R.string.default_plant_id)
        nullable = true  // default false
    }
}

Si se proporciona un defaultValue, se puede inferir el tipo. Si se proporcionan defaultValue y type, los tipos deben coincidir. Consulta la documentación de referencia de NavType para obtener una lista completa de los tipos de argumentos disponibles.

Cómo proporcionar tipos personalizados

Algunos tipos, como ParcelableType y SerializableType, no admiten el análisis de valores de las cadenas usadas por rutas o vínculos directos. Esto se debe a que no se basan en la reflexión durante el tiempo de ejecución. Si proporcionas una clase NavType personalizada, puedes controlar exactamente cómo se analiza tu tipo desde una ruta o un vínculo directo. Esto te permite usar la serialización de Kotlin y otras bibliotecas para proporcionar codificación y decodificación sin reflejo de tu tipo personalizado.

Por ejemplo, una clase de datos que representa los parámetros de búsqueda que se pasan a la pantalla de búsqueda podría implementar Serializable (para admitir codificación y decodificación) y Parcelize (para admitir el guardado y el restablecimiento desde un Bundle):

@Serializable
@Parcelize
data class SearchParameters(
  val searchQuery: String,
  val filters: List<String>
)

Un NavType personalizado se podría escribir de la siguiente manera:

val SearchParametersType = object : NavType<SearchParameters>(
  isNullableAllowed = false
) {
  override fun put(bundle: Bundle, key: String, value: SearchParameters) {
    bundle.putParcelable(key, value)
  }
  override fun get(bundle: Bundle, key: String): SearchParameters {
    return bundle.getParcelable(key) as SearchParameters
  }

  override fun parseValue(value: String): SearchParameters {
    return Json.decodeFromString<SearchParameters>(value)
  }

  // Only required when using Navigation 2.4.0-alpha07 and lower
  override val name = "SearchParameters"
}

Luego, puedes usarlo en la DSL de Kotlin como cualquier otro tipo:

fragment<SearchFragment>(nav_routes.plant_search) {
    label = getString(R.string.plant_search_title)
    argument(nav_arguments.search_parameters) {
        type = SearchParametersType
        defaultValue = SearchParameters("cactus", emptyList())
    }
}

En este ejemplo, se usa la serialización de Kotlin para analizar el valor de la cadena, lo que significa que la serialización de Kotlin también debe usarse cuando navegas al destino para garantizar que los formatos coincidan:

val params = SearchParameters("rose", listOf("available"))
val searchArgument = Uri.encode(Json.encodeToString(params))
navController.navigate("${nav_routes.plant_search}/$searchArgument")

El parámetro se puede obtener de los argumentos del destino:

val params: SearchParameters? = arguments?.getParcelable(nav_arguments.search_parameters)

Vínculos directos

Los vínculos directos se pueden agregar a cualquier destino, al igual que con un gráfico de navegación basado en XML. Todos los procedimientos que se definen en Cómo crear un vínculo directo para un destino se aplican también al proceso de creación de un vínculo directo explícito con la DSL de Kotlin.

Sin embargo, cuando creas un vínculo directo implícito, no tienes un recurso de navegación XML que se pueda analizar para los elementos <deepLink>. Por lo tanto, no puedes optar por colocar un elemento <nav-graph> en el archivo AndroidManifest.xml, sino que debes agregar filtros de intents a la actividad de forma manual. El filtro de intents que proporciones deberá coincidir con el patrón de URL base, la acción y el tipo de MIME de los vínculos directos de la app.

Puedes proporcionar un deeplink más específico para cada destino con vinculación directa individual a través de la función DSL deepLink(). Esta función acepta un objeto NavDeepLink que contiene una String que representa el patrón de URI, una String que representa las acciones de intent y una String que representa el tipo de MIME.

Por ejemplo:

deepLink {
    uriPattern = "http://www.example.com/plants/"
    action = "android.intent.action.MY_ACTION"
    mimeType = "image/*"
}

No hay límite para la cantidad de vínculos directos que puedes agregar. Cada vez que llamas a deepLink(), se agrega un vínculo directo nuevo a una lista que se mantiene para ese destino.

A continuación, se muestra una situación de vínculo directo implícito más compleja que también define parámetros basados en rutas y consultas:

val baseUri = "http://www.example.com/plants"

fragment<PlantDetailFragment>(nav_routes.plant_detail) {
   label = getString(R.string.plant_details_title)
   deepLink(navDeepLink {
    uriPattern = "${baseUri}/{id}"
   })
   deepLink(navDeepLink {
    uriPattern = "${baseUri}/{id}?name={plant_name}"
   })
}

Puedes usar la interpolación de cadenas para simplificar la definición.

Limitaciones

El complemento Safe Args no es compatible con la DSL de Kotlin, ya que busca archivos de recursos XML para generar las clases Directions y Arguments.

Más información

Consulta la página Seguridad de tipos de Navigation para obtener información sobre cómo brindar seguridad de tipos a la DSL de Kotlin y el código de Navigation Compose.