Créer un graphe par programmation à l'aide du langage DSL Kotlin

Le composant Navigation fournit un langage spécifique au domaine (ou DSL) basé sur Kotlin, qui repose sur les compilateurs sécurisés de Kotlin. Cette API vous permet de créer votre graphe de manière déclarative dans le code Kotlin plutôt que dans une ressource XML. Cette approche peut être utile si vous souhaitez créer la navigation de votre application de manière dynamique. Par exemple, votre application peut télécharger et mettre en cache une configuration de navigation à partir d'un service Web externe, puis utiliser cette configuration pour créer un graphe de navigation dynamique dans la fonction onCreate() de votre activité.

Dépendances

Pour utiliser le langage DSL Kotlin, ajoutez la dépendance suivante au fichier build.gradle de votre application :

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")
}

Créer un graphe

Commençons par un exemple de base basé sur l'application Sunflower. Pour cet exemple, nous avons deux destinations : home et plant_detail. La destination home est présente lorsque l'utilisateur lance l'application pour la première fois. Elle affiche la liste des plantes du jardin de l'utilisateur. Lorsque l'utilisateur sélectionne l'une des plantes, l'application accède à la destination plant_detail.

La figure 1 illustre ces destinations, ainsi que les arguments requis par la destination plant_detail et une action, to_plant_detail, que l'application utilise pour passer de home à plant_detail.

L'application Sunflower comprend deux destinations reliées par une action.
Figure 1 : L'application Sunflower comporte deux destinations, home et plant_detail, ainsi qu'une action qui les relie entre elles.

Héberger un graphe de navigation DSL Kotlin

Avant de pouvoir créer le graphe de navigation de votre application, vous avez besoin d'un emplacement pour l'héberger. Cet exemple utilise des fragments. Il héberge donc le graphe dans un élément NavHostFragment situé dans un objet 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>

Notez que l'attribut app:navGraph n'est pas défini dans cet exemple. Le graphe n'est pas défini en tant que ressource dans le dossier res/navigation. Il doit donc être défini dans le cadre du processus onCreate() de l'activité.

En XML, une action associe un ID de destination à un ou plusieurs arguments. Toutefois, lorsque vous utilisez le langage DSL de navigation, des arguments peuvent être inclus dans la route. Autrement dit, il n'existe pas de concept d'action lorsque vous utilisez le langage DSL.

L'étape suivante consiste à spécifier des constantes que vous utiliserez pour définir votre graphe.

Créer des constantes pour votre graphe

Les graphes de navigation XML sont analysés dans le cadre du processus de compilation Android. Une constante numérique est créée pour chaque attribut id défini dans le graphe. Ces ID statiques générés au moment de la compilation ne sont pas disponibles lors de la création du graphe de navigation au moment de l'exécution. Par conséquent, le langage DSL de navigation utilise des chaînes de routage au lieu d'ID. Chaque route est représentée par une chaîne unique qu'il est recommandé de définir comme constante pour réduire le risque de bugs liés à des fautes de frappe.

Dans le cas des arguments, ceux-ci sont intégrés à la chaîne de routage. Comme mentionnée précédemment, l'intégration de cette logique dans la route permet de réduire le risque de bugs liés à des fautes de frappe.

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"
}

Une fois que vous avez défini les constantes, vous pouvez créer le graphe de navigation.

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
        }
    }
}

Dans cet exemple, le lambda final définit deux destinations de fragment à l'aide de la fonction de compilateur DSL fragment(). Cette fonction requiert une chaîne de routage, obtenue à partir des constantes. La fonction accepte également un lambda facultatif pour une configuration supplémentaire, telle que le libellé de la destination, ainsi que des fonctions de compilateur intégrées pour les arguments et les liens profonds.

La classe Fragment qui gère l'interface utilisateur de chaque destination est transmise en tant que type paramétré entre crochets (<>). Cela revient à spécifier l'attribut android:name sur les destinations de fragment définies au format XML.

Enfin, vous pouvez passer de home à plant_detail à l'aide des appels NavController.redirect() standards :

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

Dans PlantDetailFragment, vous pouvez obtenir la valeur de l'argument comme illustré dans l'exemple suivant :

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

Pour découvrir comment fournir des arguments lors de la navigation, consultez la section Fournir des arguments de destination.

Le reste de ce guide décrit les éléments courants du graphe de navigation, les destinations et leur utilisation lors de la création du graphe.

Destinations

Le langage DSL Kotlin est compatible avec trois types de destinations : Fragment, Activity et NavGraph. Chacune d'elles dispose de sa propre fonction d'extension intégrée permettant sa compilation et sa configuration.

Destination "fragment"

La fonction DSL fragment() peut être paramétrée pour correspondre à la classe de fragment d'implémentation. Elle utilise une chaîne de route unique à attribuer à cette destination, suivie d'un lambda où vous pouvez fournir une configuration supplémentaire, comme décrit dans la section Navigation avec le graphe DSL Kotlin.

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

Destination "activity"

La fonction DSL activity() accepte une chaîne de route unique à attribuer à cette destination, mais n'est paramétrée pour aucune classe d'activité d'implémentation. À la place, vous devez définir un élément activityClass facultatif dans un lambda de fin. Cette flexibilité vous permet de spécifier une destination "activity" à lancer à l'aide d'un intent implicite, lorsqu'une classe d'activité explicite n'est pas justifiée. Comme pour les destinations "fragment", vous pouvez également configurer un libellé, des arguments et des liens profonds.

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

   activityClass = ActivityDestination::class
}

La fonction DSL navigation() permet de créer un graphe de navigation imbriqué. Cette fonction accepte trois arguments : une route à attribuer au graphe, la route de la destination de départ du graphe et un lamba pour configurer davantage le graphe. Parmi les éléments valides, citons d'autres destinations, des arguments, des liens profonds et un libellé descriptif de la destination. Ce libellé peut être utile pour relier le graphe de navigation à des composants d'UI à l'aide de NavigationUI

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

Destinations personnalisées compatibles

Si vous utilisez un nouveau type de destination qui n'est pas directement compatible avec le langage DSL Kotlin, vous pouvez l'ajouter à votre langage DSL Kotlin via addDestination() :

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

Vous pouvez également utiliser l'opérateur unaire plus (+) pour ajouter une destination que vous venez de créer directement au graphe :

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

Fournir des arguments de destination

Toute destination peut définir des arguments facultatifs ou obligatoires. Les actions peuvent être définies à l'aide de la fonction argument() au niveau de NavDestinationBuilder, qui est la classe de base pour tous les types de compilateurs de destination. Cette fonction utilise le nom de l'argument sous la forme d'une chaîne et d'un lambda utilisé pour construire et configurer un élément NavArgument.

Dans le lambda, vous pouvez spécifier le type de données d'argument, une valeur par défaut, le cas échéant, et indiquer si cette valeur peut être nulle.

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 defaultValue est fourni, le type peut être déduit. Si defaultValue et type sont fournis, les types doivent correspondre. Consultez la documentation de référence sur NavType pour obtenir la liste complète des types d'arguments disponibles.

Fournir des types personnalisés

Certains types, tels que ParcelableType et SerializableType, ne permettent pas l'analyse des valeurs des chaînes utilisées par des routes ou des liens profonds. Cela est dû au fait qu'ils ne s'appuient pas sur la réflexion au moment de l'exécution. En fournissant une classe NavType personnalisée, vous pouvez contrôler exactement la manière dont le type est analysé à partir d'un routage ou d'un lien profond. Cela vous permet d'utiliser la sérialisation Kotlin ou d'autres bibliothèques pour fournir un encodage et un décodage de votre type personnalisé sans réflexion.

Par exemple, une classe de données qui représente la transmission de paramètres de recherche à votre écran de recherche peut implémenter à la fois Serializable (pour permettre l'encodage et le décodage) et Parcelize (pour permettre l'enregistrement et la restauration à partir d'un élément Bundle) :

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

Un élément NavType personnalisé peut être écrit comme suit :

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"
}

Vous pouvez ensuite l'utiliser dans le langage DSL Kotlin comme n'importe quel autre type :

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

Cet exemple utilise la sérialisation Kotlin pour analyser la valeur de la chaîne. Autrement dit, la sérialisation Kotlin doit également être utilisée lorsque vous accédez à la destination pour vous assurer que les formats correspondent :

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

Le paramètre peut être obtenu à partir des arguments de la destination :

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

Liens profonds

Des liens profonds peuvent être ajoutés à n'importe quelle destination, tout comme avec un graphe de navigation basé sur XML. Toutes les procédures définies dans la section Créer un lien profond pour une destination s'appliquent au processus de création d'un lien profond explicite à l'aide de DSL Kotlin.

Toutefois, lorsque vous créez un lien profond implicite, vous ne disposez d'aucune ressource de navigation XML dont les éléments <deepLink> peuvent être analysés. Par conséquent, vous ne pouvez pas compter sur le placement d'un élément <nav-graph> dans votre fichier AndroidManifest.xml. Vous devez ajouter manuellement des filtres d'intent à votre activité. Le filtre d'intent que vous fournissez doit correspondre au format d'URL de base, à l'action et au type MIME des liens profonds de votre application.

Vous pouvez fournir un élément deeplink plus spécifique pour chaque destination de lien profond à l'aide de la fonction DSL deepLink(). Cette fonction accepte un NavDeepLink qui contient un élément String représentant le format d'URI, un élément String représentant les actions d'intent et un élément String représentant le type MIME.

Par exemple :

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

Vous pouvez ajouter autant de liens profonds que vous le souhaitez. Chaque fois que vous appelez deepLink(), un nouveau lien profond est ajouté à la liste correspondant à cette destination.

Vous trouverez ci-dessous un scénario plus complexe de liens profonds implicites, qui définit également les paramètres basés sur les chemins et les requêtes :

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}"
   })
}

Vous pouvez utiliser l'interpolation de chaîne pour simplifier la définition.

Limites

Le plug-in Safe Args n'est pas compatible avec le langage DSL Kotlin, car il recherche des fichiers de ressources XML pour générer les classes Directions et Arguments.

En savoir plus

Consultez la page Sûreté du typage dans Navigation pour découvrir comment sécuriser votre code DSL Kotlin et Navigation Compose.