Fragmentos y 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 tu gráfico de forma declarativa en tu código Kotlin, en lugar de hacerlo dentro de un recurso XML. Esto puede ser útil si deseas compilar la navegación de tu app de forma dinámica. Por ejemplo, tu app podría descargar y almacenar en caché una configuración de navegación de un servicio web externo y, luego, usar esa configuración para compilar de forma dinámica un gráfico de navegación en la función onCreate() de tu actividad.

Dependencias

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

Groovy

dependencies {
    def nav_version = "2.10.2"

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

Kotlin

dependencies {
    val nav_version = "2.10.2"

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

Cómo compilar un gráfico

Este es un ejemplo básico basado en la app de Sunflower. En 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 al 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 las rutas que usarás cuando definas tu gráfico.

Cómo crear rutas para tu 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 el DSL de Navigation usa tipos serializables en lugar de IDs. Cada ruta se representa con un tipo único.

Los argumentos están integrados en el tipo de ruta. Esto te permite tener seguridad de tipos para tus argumentos de navegación.

@Serializable data object Home
@Serializable data class Plant(val id: String)

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

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
    startDestination = Home
) {
    fragment<HomeFragment, Home> {
        label = resources.getString(R.string.home_title)
    }
    fragment<PlantDetailFragment, PlantDetail> {
        label = resources.getString(R.string.plant_detail_title)
    }
}

En este ejemplo, se definen dos destinos de fragmentos con la función de compilador de DSL fragment(). Esta función requiere dos argumentos de tipo.

Primero, una clase Fragment que proporciona la IU para este destino. Configurar esto tiene el mismo efecto que configurar el atributo android:name en los destinos de fragmentos que se definen con XML.

En segundo lugar, la ruta. Debe ser un tipo serializable que se extienda desde Any. Debe contener los argumentos de navegación que usará este destino y sus tipos.

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 personalizados y vínculos directos.

Por último, puedes navegar de home a plant_detail con llamadas NavController.navigate():

private fun navigateToPlant(plantId: String) {
   findNavController().navigate(route = PlantDetail(id = plantId))
}

En PlantDetailFragment, puedes obtener los argumentos de navegación obteniendo el NavBackStackEntry actual y llamando a toRoute en él para obtener la instancia de ruta.

val plantDetailRoute = findNavController().getBackStackEntry<PlantDetail>().toRoute<PlantDetail>()
val plantId = plantDetailRoute.id

Si PlantDetailFragment usa un ViewModel, obtén la instancia de ruta con SavedStateHandle.toRoute.

val plantDetailRoute = savedStateHandle.toRoute<PlantDetail>()
val plantId = plantDetailRoute.id

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() se puede parametrizar con la clase de fragmento para la IU y el tipo de ruta que se usa para identificar de forma única este destino, seguido de una lambda en la que puedes proporcionar una configuración adicional, como se describe en la sección Cómo navegar con tu gráfico DSL de Kotlin.

fragment<MyFragment, MyRoute> {
   label = getString(R.string.fragment_title)
   // custom argument types, deepLinks
}

Destino de la actividad

La función DSL activity() toma un parámetro de tipo para la ruta, 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 personalizados y vínculos directos.

activity<MyRoute> {
   label = getString(R.string.activity_title)
   // custom argument types, deepLinks...

   activityClass = MyActivity::class
}

La función DSL navigation() se puede usar para compilar un gráfico de navegación anidado. Esta función toma un parámetro de tipo para la ruta que se asignará a este gráfico. También toma dos argumentos: 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, tipos de argumentos personalizados, 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.

@Serializable data object HomeGraph
@Serializable data object Home

navigation<HomeGraph>(startDestination = 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 con 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

Los argumentos de destino se pueden definir como parte de la clase de ruta. Se pueden definir de la misma manera que lo harías para cualquier clase de Kotlin. Los argumentos obligatorios se definen como tipos que no admiten valores nulos, y los argumentos opcionales se definen con valores predeterminados.

El mecanismo subyacente para representar rutas y sus argumentos se basa en cadenas. El uso de cadenas para modelar rutas permite que el estado de navegación se almacene y se restablezca desde el disco durante los cambios de configuración y el cierre del proceso iniciado por el sistema. Por este motivo, cada argumento de navegación debe ser serializable, es decir, debe tener un método que convierta la representación en memoria del valor del argumento en un String.

El complemento de serialización de Kotlin genera automáticamente métodos de serialización para tipos básicos cuando se agrega la anotación @Serializable a un objeto.

@Serializable
data class MyRoute(
  val id: String,
  val myList: List<Int>,
  val optionalArg: String? = null
)

fragment<MyFragment, MyRoute>

Cómo proporcionar tipos personalizados

Para los tipos de argumentos personalizados, deberás proporcionar una clase NavType personalizada. Esto te permite controlar exactamente cómo se analiza tu tipo desde una ruta o un vínculo directo.

Por ejemplo, una ruta que se usa para definir una pantalla de búsqueda podría contener una clase que represente los parámetros de búsqueda:

@Serializable
data class SearchRoute(val parameters: SearchParameters)

@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 serializeAsValue(value: SearchParameters): String {
    // Serialized values must always be Uri encoded
    return Uri.encode(Json.encodeToString(value))
  }

  override fun parseValue(value: String): SearchParameters {
    // Navigation takes care of decoding the string
    // before passing it to parseValue()
    return Json.decodeFromString<SearchParameters>(value)
  }
}

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

fragment<SearchFragment, SearchRoute>(
    typeMap = mapOf(typeOf<SearchParameters>() to SearchParametersType)
) {
    label = getString(R.string.plant_search_title)
}

Cuando navegues al destino, crea una instancia de tu ruta:

val params = SearchParameters("rose", listOf("available"))
navController.navigate(route = SearchRoute(params))

El parámetro se puede obtener de la ruta en el destino:

val searchRoute = navController().getBackStackEntry<SearchRoute>().toRoute<SearchRoute>()
val params = searchRoute.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 con el 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 la ruta de acceso base, la acción y el tipo de MIME de los vínculos directos de la app.

Los vínculos directos se agregan a un destino llamando a la función deepLink dentro de la lambda del destino. Acepta la ruta como un tipo parametrizado y un parámetro basePath para la ruta de acceso base de la URL que se usa para el vínculo directo.

También puedes agregar una acción y un tipo de MIME con la lambda final deepLinkBuilder.

En el siguiente ejemplo, se crea un URI de vínculo directo para el destino Home.

@Serializable data object Home

fragment<HomeFragment, Home>{
  deepLink<Home>(basePath = "www.example.com/home"){
    // Optionally, specify the action and/or mime type that this destination
    // supports
    action = "android.intent.action.MY_ACTION"
    mimeType = "image/*"
  }
}

Formato del URI

El formato del URI del vínculo directo se genera automáticamente a partir de los campos de la ruta con las siguientes reglas:

  • Los parámetros obligatorios se agregan como parámetros de ruta (ejemplo: /{id}).
  • Los parámetros con un valor predeterminado (parámetros opcionales) se agregan como parámetros de consulta (ejemplo: ?name={name}).
  • Las colecciones se agregan como parámetros de consulta (ejemplo: ?items={value1}&items={value2}).
  • El orden de los parámetros coincide con el orden de los campos en la ruta.

Por ejemplo, el siguiente tipo de ruta:

@Serializable data class PlantDetail(
  val id: String,
  val name: String,
  val colors: List<String>,
  val latinName: String? = null,
)

tiene el siguiente formato de URI generado:

basePath/{id}/{name}/?colors={color1}&colors={color2}&latinName={latinName}

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.

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.