Fragments et 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 composer votre graphique de manière déclarative dans votre code Kotlin, 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 DSL Kotlin avec des fragments, ajoutez la dépendance suivante au fichier Fichier build.gradle:

Groovy

dependencies {
    def nav_version = "2.9.3"

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

Kotlin

dependencies {
    val nav_version = "2.9.3"

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

Créer un graphe

Voici un exemple basique basé sur le modèle Tournesol l'application Nest. Pour cette Par 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 DSL de navigation, un itinéraire peut contenir des arguments la route. Autrement dit, il n'existe pas de concept d'action lorsque vous utilisez le langage DSL.

L'étape suivante consiste à définir les routes à utiliser lors de la définition graphique.

Créer des itinéraires pour votre graphique

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 graphique. Ces ID statiques générés lors de la compilation ne sont pas disponible lorsque vous créez votre graphique de navigation au moment de l'exécution. Ainsi, le langage DSL de navigation utilise des valeurs sérialisables d'entraînement au lieu de ID. Chaque itinéraire est représenté par un type unique.

Dans le cas des arguments, ceux-ci sont intégrés au type de route. Vous bénéficiez ainsi de la sûreté du typage pour vos arguments de navigation.

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

Une fois que vous avez défini vos itinéraires, vous pouvez créer le graphique de navigation.

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

Dans cet exemple, deux destinations de fragment sont définies à l'aide de la fonction de compilateur DSL fragment(). Cette fonction requiert deux types arguments pour en savoir plus.

Tout d'abord, une classe Fragment qui fournit l'UI de cette destination. La définition de ce paramètre a le même effet que Définir l'attribut android:name sur les destinations de fragment définies au format XML.

Deuxièmement, l'itinéraire. Il doit s'agir d'un type sérialisable qui s'étend à partir de Any. Il doit contenir tous les arguments de navigation qui seront utilisés par cette destination, et leurs types.

La fonction accepte également un lambda facultatif pour une configuration supplémentaire, telle que comme libellé de destination, ainsi que des fonctions de compilateur intégrées pour des les arguments et les liens profonds.

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

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

Dans PlantDetailFragment, vous pouvez obtenir les arguments de navigation en obtenant l'actuel NavBackStackEntry et appeler toRoute pour obtenir l'instance de route.

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

Si PlantDetailFragment utilise un ViewModel, obtenez l'instance de route à l'aide de SavedStateHandle.toRoute.

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

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 fragment() La fonction DSL peut être paramétrée avec la classe de fragment pour l'interface utilisateur et la fonction type d'itinéraire utilisé pour identifier cette destination de manière unique, suivi d'un lambda où vous pouvez fournir une configuration supplémentaire, comme décrit dans la section Navigation avec votre graphique DSL Kotlin.

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

Destination "activity"

La fonction DSL activity() accepte un paramètre de type pour le parcours, mais n'est pas 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 définir une destination d'activité pour une activité qui doit être lancée à 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 personnalisés et des liens profonds.

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

   activityClass = MyActivity::class
}

La fonction DSL navigation() permet de créer un graphe de navigation imbriqué. Cette fonction accepte un type pour l'itinéraire à attribuer à ce graphique. Elle accepte également deux arguments : la route de la destination de départ du graphe et un lambda pour configurer davantage le graphe. Parmi les éléments valides, citons d'autres destinations, des types d'arguments personnalisés, des liens profonds et un libellé descriptif de la destination. Cette étiquette peut être utile pour lier le graphique de navigation aux composants d'interface utilisateur à l'aide de NavigationUI

@Serializable data object HomeGraph
@Serializable data object Home

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

Les arguments de destination peuvent être définis dans la classe d'itinéraire. Ils peuvent être définis de la même manière que pour n'importe quelle classe Kotlin. Les arguments obligatoires sont définis comme des types ne pouvant pas avoir une valeur nulle, et les arguments facultatifs sont définis avec valeurs.

Le mécanisme sous-jacent permettant de représenter les routes et leurs arguments est basé sur des chaînes. L'utilisation de chaînes pour modéliser les routes permet de stocker l'état de navigation et à partir du disque lors de la configuration modifications et le processus initié par le système la mort. Pour cette raison, chaque argument de navigation doit être sérialisable, c'est-à-dire qu'il doit avoir une qui convertit la représentation en mémoire de la valeur d'argument en String

Le plug-in de sérialisation Kotlin génère automatiquement des méthodes de sérialisation pour les types de base lorsque l'annotation @Serializable est ajoutée à un objet.

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

fragment<MyFragment, MyRoute>

Fournir des types personnalisés

Pour les types d'arguments personnalisés, vous devez fournir une classe NavType personnalisée. Ce vous permet de contrôler précisément la façon dont votre type est analysé à partir d'un itinéraire ou d'un lien profond.

Par exemple, un itinéraire utilisé pour définir un écran de recherche peut contenir une classe qui représente les paramètres de recherche:

@Serializable
data class SearchRoute(val parameters: SearchParameters)

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

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

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

Lorsque vous vous dirigez vers la destination, créez une instance de votre itinéraire :

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

Le paramètre peut être obtenu à partir de l'itinéraire dans la destination:

val searchRoute = navController().getBackStackEntry<SearchRoute>().toRoute<SearchRoute>()
val params = searchRoute.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 à l'aide de DSL Kotlin.

Lorsque vous créez un lien profond implicite Toutefois, vous ne disposez d'aucune ressource de navigation XML pouvant être analysée <deepLink>. 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 chemin d'accès de base, à l'action et au type MIME des liens profonds de votre application.

Les liens profonds sont ajoutés à une destination en appelant la fonction deepLink dans le lambda de la destination. Il accepte l'itinéraire en tant que type paramétré et un paramètre basePath pour le chemin d'accès de base de l'URL utilisée pour le lien profond.

Vous pouvez également ajouter une action et un mimetype à l'aide du lambda de fin deepLinkBuilder.

L'exemple suivant crée un URI de lien profond pour la destination 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/*"
  }
}

Format de l'URI

Le format de l'URI du lien profond est généré automatiquement à partir des champs de la route à l'aide des règles suivantes :

  • Les paramètres obligatoires sont ajoutés en tant que paramètres de chemin (exemple: /{id}).
  • Les paramètres avec une valeur par défaut (paramètres facultatifs) sont ajoutés en tant que paramètres de requête (par exemple, ?name={name}).
  • Les collections sont ajoutées en tant que paramètres de requête (exemple: ?items={value1}&items={value2}).
  • L'ordre des paramètres correspond à l'ordre des champs dans le parcours.

Par exemple, le type de route suivant :

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

possède le format d'URI généré suivant:

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

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.

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.