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.5.3"

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

Kotlin

dependencies {
    val nav_version = "2.5.3"

    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 crear el host para tu gráfico de navegación del DSL de Kotlin

Independientemente de cómo compiles tu gráfico, debes alojarlo en un objeto NavHost. Sunflower utiliza fragmentos, así que usemos un objeto NavHostFragment dentro de un elemento FragmentContainerView, como se muestra en el siguiente ejemplo:

<!-- 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 está configurado en este ejemplo, ya que el gráfico se compila de manera programática, en lugar de definirse como un recurso XML.

Cómo crear constantes para el gráfico

Cuando trabajas con gráficos de navegación basados en XML, el proceso de compilación de Android analiza el archivo de recursos del gráfico y define las constantes numéricas para cada atributo id definido en el gráfico. Puedes acceder a estas constantes en tu código por medio de una clase de recurso generada, R.id.

Por ejemplo, el siguiente fragmento de gráfico XML declara un destino de fragmento con un objeto id, home:

<navigation ...>
   <fragment android:id="@+id/home" ... />
   ...
</navigation>

El proceso de compilación crea un valor constante, R.id.home, que se asocia con este destino. Luego, puedes hacer referencia a este destino desde el código mediante este valor constante.

Este proceso de análisis y generación de constantes no ocurre cuando compilas un gráfico de manera programática con el DSL de Kotlin. En cambio, debes definir tus propias constantes para cada destino, acción y argumento que tenga un valor id. Cada ID debe ser único y coherente en todas las modificaciones de configuración.

Una forma organizada de crear constantes es crear un conjunto anidado de elementos object de Kotlin que defina las constantes de forma estática, como se muestra en el siguiente ejemplo:

object nav_graph {

    const val id = 1 // graph id

    object dest {
        const val home = 2
        const val plant_detail = 3
    }

    object action {
        const val to_plant_detail = 4
    }

    object args {
        const val plant_id = "plantId"
    }
}

Con esta estructura, puedes encadenar las llamadas del objeto para acceder a los valores de ID en el código, como se muestra en los siguientes ejemplos:

nav_graph.id                     // graph id
nav_graph.dest.home              // home destination id
nav_graph.action.to_plant_detail // action home -> plant_detail id
nav_graph.args.plant_id          // destination argument name

Una vez que hayas definido el conjunto inicial de ID, puedes crear el gráfico de navegación. Usa la función de extensión NavController.createGraph() para crear un objeto NavGraph pasando un id para el gráfico, un valor de ID para el elemento startDestination y una expresión lambda final que defina la estructura del gráfico.

Puedes compilar el gráfico en la función onCreate() de tu actividad. createGraph() muestra un objeto Navgraph que puedes asignar a la propiedad graph del elemento NavController que está asociado a tu NavHost, como se muestra en el siguiente ejemplo:

class GardenActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_garden)

        val navHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host) as NavHostFragment

        navHostFragment.navController.apply {
            graph = createGraph(nav_graph.id, nav_graph.dest.home) {
                fragment<HomeViewPagerFragment>(nav_graph.dest.home) {
                    label = getString(R.string.home_title)
                    action(nav_graph.action.to_plant_detail) {
                        destinationId = nav_graph.dest.plant_detail
                    }
                }
                fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
                    label = getString(R.string.plant_detail_title)
                    argument(nav_graph.args.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 un ID para el destino. La función también acepta una expresión lambda opcional para la configuración adicional, como el objeto label de destino, además de funciones de compilador incorporadas para acciones, 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.

Una vez que hayas compilado y configurado el gráfico, puedes navegar desde home hasta plant_detail con el objeto NavController.navigate(), como se muestra en el siguiente ejemplo:

private fun navigateToPlant(plantId: String) {

    val args = bundleOf(nav_graph.args.plant_id to plantId)

    findNavController().navigate(nav_graph.action.to_plant_detail, args)
}

Tipos de destino admitidos

El DSL de Kotlin admite los destinos Fragment, Activity y NavGraph, cada uno de ellos con su propia función de extensión intercalada disponible para compilar y configurar el destino.

Destinos de fragmentos

Es posible atribuir parámetros a la función DSL fragment() para la clase Fragment de implementación. Esta función toma un ID único para asignar a este destino, junto con un lambda en el que puedes proporcionar una configuración adicional.

fragment<FragmentDestination>(nav_graph.dest.fragment_dest_id) {
   label = getString(R.string.fragment_title)
   // arguments, actions, deepLinks...
}

Destino de la actividad

La función DSL activity() toma un ID único para asignar a este destino, pero no se le atribuyen parámetros para ninguna clase de actividad de implementación. En su lugar, puedes configurar un objeto activityClass opcional en una expresión lambda final. Esta flexibilidad te permite definir un destino de actividad para una actividad que se inicia desde 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 definir y configurar una etiqueta y cualquier argumento.

activity(nav_graph.dest.activity_dest_id) {
    label = getString(R.string.activity_title)
    // arguments, actions, deepLinks...

    activityClass = ActivityDestination::class
}

Puedes usar la función DSL navigation() para crear un gráfico de navegación anidado. Al igual que con los otros tipos de destino, esta función DSL toma tres argumentos: un ID para asignar al gráfico, un ID de destino inicial para el gráfico y una expresión lambda de configuración adicional. Entre los elementos válidos para la expresión lambda se incluyen argumentos, acciones, otros destinos, vínculos directos y una etiqueta.

navigation(nav_graph.dest.nav_graph_dest, nav_graph.dest.start_dest) {
   // label, arguments, actions, other destinations, deep links
}

Compatibilidad con destinos personalizados

Puedes usar el objeto addDestination() para agregar tipos de destino personalizados a tu DSL de Kotlin que no son compatibles de forma predeterminada, como se muestra en el siguiente ejemplo:

// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
    id = nav_graph.dest.custom_dest_id
}
addDestination(customDestination)

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 {
    id = nav_graph.dest.custom_dest_id
}

Cómo proporcionar argumentos de destino

Puedes definir argumentos opcionales u obligatorios para cualquier tipo de destino. Si quieres definir un argumento, llama a la función argument() en NavDestinationBuilder, la clase base para todos los tipos de compiladores de destino. Esta función toma el nombre del argumento como String y una expresión lambda que puedes usar para construir y configurar un objeto NavArgument. Dentro de la expresión lambda, puedes especificar el tipo de datos del argumento, un valor predeterminado, si corresponde, y si el valor del argumento puede ser null.

fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
    label = getString(R.string.plant_details_title)
    argument(nav_graph.args.plant_name) {
        type = NavType.StringType
        defaultValue = getString(R.string.default_plant_name)
        nullable = true  // default false
    }
}

Si se proporciona un objeto defaultValue, el elemento type es opcional. En este caso, si no se especifica un elemento type, el tipo se infiere de defaultValue. Si se proporciona tanto un objeto defaultValue como type, los tipos deben coincidir. Para obtener una lista completa de los tipos de argumentos, consulta NavType.

Acciones

Puedes definir acciones en cualquier destino, incluidas las acciones globales en el gráfico de navegación raíz. Para definir una acción, usa la función NavDestinationBuilder.action() y proporciónale un ID y una expresión lambda a fin de brindar configuración adicional.

En el siguiente ejemplo, se compila una acción con un objeto destinationId, animaciones de transición y comportamiento emergente y de una sola parte superior.

action(nav_graph.action.to_plant_detail) {
    destinationId = nav_graph.dest.plant_detail
    navOptions {
        anim {
            enter = R.anim.nav_default_enter_anim
            exit = R.anim.nav_default_exit_anim
            popEnter = R.anim.nav_default_pop_enter_anim
            popExit = R.anim.nav_default_pop_exit_anim
        }
        popUpTo(nav_graph.dest.start_dest) {
            inclusive = true // default false
        }
        // if popping exclusively, you can specify popUpTo as
        // a property. e.g. popUpTo = nav_graph.dest.start_dest
        launchSingleTop = true // default false
    }
}

Vínculos directos

Puedes agregar vínculos directos a cualquier destino, así como con un gráfico de navegación basado en XML. Los mismos procedimientos que se definen en Cómo crear un vínculo directo para un destino se aplican al proceso de creación de un vínculo directo explícito 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 confiar en colocar un elemento <nav-graph> en tu archivo AndroidManifest.xml, sino que debes agregar filtros de intents de forma manual a la actividad. El filtro de intents que proporcionas debe coincidir con el patrón de URL base de los vínculos directos de la app.

Para cada destino de vínculo directo individual, puedes proporcionar un patrón de URI más específico con la función DSL deepLink(). Esta función acepta un objeto String para el patrón de URI, como se muestra en el siguiente ejemplo:

deepLink("http://www.example.com/plants/")

No hay límite para la cantidad de URI de vínculo directo que puedes agregar. Cada llamada a deepLink() agrega un nuevo vínculo directo a una lista interna específica para ese destino.

Esta es 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_graph.dest.plant_detail) {
    label = getString(R.string.plant_details_title)
    deepLink("${baseUri}/{id}")
    deepLink("${baseUri}/{id}?name={plant_name}")
    argument(nav_graph.args.plant_id) {
       type = NavType.IntType
    }
    argument(nav_graph.args.plant_name) {
        type = NavType.StringType
        nullable = true
    }
}

Ten en cuenta que se puede usar la interpolación de strings para simplificar la definición.

Cómo crear ID

La biblioteca de Navigation requiere que los valores de ID que se usan para los elementos del gráfico sean valores enteros únicos que permanecen constantes a través de los cambios de configuración. Una forma de crear estos ID es definirlos como constantes estáticas, según se muestra en Cómo crear constantes para el gráfico. También puedes definir ID de recursos estáticos en XML como un recurso. De manera alternativa, puedes construir ID de forma dinámica. Por ejemplo, puedes crear un contador de secuencias que aumente cada vez que hagas referencia a él.

object nav_graph {
    // Counter for id's. First ID will be 1.
    var id_counter = 1

    val id = id_counter++

    object dest {
       val home = id_counter++
       val plant_detail = id_counter++
    }

    object action {
       val to_plant_detail = id_counter++
    }

    object args {
       const val plant_id = "plantId"
    }
}

Limitaciones

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