Pour faire correspondre des URI hiérarchiques à des modèles et extraire des arguments, utilisez
UriDeepLinkMatcher. Il s'appuie sur kotlinx.serialization pour désérialiser les arguments correspondants dans vos classes de clés.
Pour créer un UriDeepLinkMatcher, fournissez un modèle DeepLinkUri et le
sérialiseur de la clé correspondante :
@Serializable data class UserProfileKey(val id: String) : NavKey val userProfilePattern = DeepLinkUri("www.example.com/users/{id}") val userProfileMatcher = UriDeepLinkMatcher(userProfilePattern, serializer<UserProfileKey>()) val request = DeepLinkRequest(uri = "https://www.example.com/users/123") val matchResult = userProfileMatcher.match(request) val key = matchResult?.key // UserProfileKey(id = "123")
Pour les URI non hiérarchiques ou les schémas personnalisés (tels que tel:), consultez
Créer des comparateurs de liens profonds personnalisés.
Modèles de correspondance acceptés
UriDeepLinkMatcher fait correspondre les URI en fonction de leurs cinq composants : schéma, autorité, chemin, requête et fragment. Les sections suivantes décrivent la syntaxe des modèles acceptés, les espaces réservés pour les arguments et les règles de correspondance pour chaque composant.
Correspondance de schéma
Si aucun schéma n'est présent dans le modèle d'URI, http et https sont mis en correspondance.
Pour faire correspondre un schéma spécifique, incluez-le dans le modèle. Exceptionnellement, un
http schéma dans un modèle correspond aux URI de requête http et https, tandis que
https dans un modèle ne correspond qu'aux requêtes https.
| URI du modèle | URI de la requête | Correspondance |
|---|---|---|
www.example.com |
https://www.example.com |
✅ |
www.example.com |
http://www.example.com |
✅ |
http://www.example.com |
http://www.example.com |
✅ |
http://www.example.com |
https://www.example.com |
✅ |
https://www.example.com |
http://www.example.com |
❌ |
myapp://www.example.com |
myapp://www.example.com |
✅ |
Correspondance d'autorité
UriDeepLinkMatcher effectue une correspondance exacte non sensible à la casse sur l'autorité de l'URI (hôte et port facultatif). Les espaces réservés ou les caractères génériques ne sont pas acceptés dans l'autorité, et aucun argument n'est extrait :
| URI du modèle | URI de la requête | Correspondance |
|---|---|---|
example.com |
https://example.com |
✅ |
example.com |
https://EXAMPLE.COM |
✅ |
example.com |
https://sub.example.com |
❌ |
example.com |
https://www.example.com |
❌ |
example.com |
https://example.com:8080 |
❌ |
example.com:8080 |
https://example.com:8080 |
✅ |
example.com:8080 |
https://example.com |
❌ |
Correspondance de chemin
Les modèles de chemin suivants sont acceptés :
| URI du modèle | URI de la requête | Correspondance | Arguments extraits |
|---|---|---|---|
www.example.com/users |
https://www.example.com/users |
✅ | Aucun |
www.example.com/users/{id} |
https://www.example.com/users/123 |
✅ | id: "123" |
www.example.com/users/{first}-{last} |
https://www.example.com/users/john-doe |
✅ | first: "john", last: "doe" |
www.example.com/users/{id}/profile |
https://www.example.com/users//profile |
✅ | id: "" (chaîne vide) |
www.example.com/users/user_{id} |
https://www.example.com/users/user_123 |
✅ | id: "123" |
www.example.com/users/{userId}/posts/{postId} |
https://www.example.com/users/123/posts/456 |
✅ | userId: "123", postId: "456" |
www.example.com/users/.* |
https://www.example.com/users/john-doe |
✅ | Aucun |
www.example.com/users |
https://www.example.com/users/ |
❌ (La barre oblique finale crée un segment supplémentaire) | N/A |
Correspondance de requête
L'ordre des paramètres de requête dans l'URI de la demande ne doit pas nécessairement correspondre à l'ordre dans l'URI du modèle. De plus, les paramètres présents dans l'URI de la demande, mais pas dans l'URI du modèle, sont ignorés.
Les modèles de paramètres de requête suivants sont acceptés :
| URI du modèle | URI de la requête | Arguments extraits |
|---|---|---|
www.example.com/users?name={name} |
https://www.example.com/users?name=john |
name: "john" |
www.example.com/users?name={name} |
https://www.example.com/users?name= |
name: "" (chaîne vide) |
www.example.com/users?{rawQuery} |
https://www.example.com/users?anything&else |
rawQuery: ["anything", "else"] |
www.example.com/users?type=user_{id} |
https://www.example.com/users?type=user_123 |
id: "123" |
www.example.com/users?name={first}_{last} |
https://www.example.com/users?name=john_doe |
first: "john", last: "doe" |
www.example.com/users?list={list} |
https://www.example.com/users?list=10&list=20 |
list: ["10", "20"] |
www.example.com/users?name={name}&{other} |
https://www.example.com/users?name=john&tab=info |
name: "john", other: ["tab=info"] |
www.example.com/users?type=user_.* |
https://www.example.com/users?type=user_admin |
type: "admin" |
Correspondance de fragment
Les types de modèles de fragments suivants sont acceptés :
| URI du modèle | URI de la requête | Arguments extraits |
|---|---|---|
www.example.com/#section1 |
https://www.example.com/#section1 |
Aucun |
www.example.com/#section_{id} |
https://www.example.com/#section_123 |
id: "123" |
www.example.com/#section_.* |
https://www.example.com/#section_123 |
Aucun |
Types de données acceptés
UriDeepLinkMatcher permet de désérialiser les arguments d'URI en types primitifs, enums, collections et objets personnalisés. La sérialisation se divise en deux catégories :
- Sérialisation standard : utilise
kotlinx.serializationpour désérialiser en :- Primitifs (
Boolean,Int,Long,Float,Double,Char,Byte,Short) etString - Enums
Set,ListouArrayde primitifs, de chaînes ou d'enums- Classes
@Serializableimbriquées (dont les propriétés sont aplaties en espaces réservés d'URI individuels)
- Primitifs (
- Sérialisation personnalisée avec
DeepLinkSerializer: conversion entre un seulStringet des objets personnalisés, des types externes (tels quejava.time.LocalDate) ou des collections délimitées personnalisées.
Sérialisation standard
UriDeepLinkMatcher fonctionne immédiatement pour les types standards et les structures aplaties sans nécessiter d'implémentations de sérialiseur personnalisées.
Primitifs et chaînes
UriDeepLinkMatcher décode automatiquement les types primitifs (Boolean, Int, Long, Float, Double, Char, Byte, Short) et String :
@Serializable data class UserProfileKey(val id: Int) : NavKey val matcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/users/{id}"), serializer<UserProfileKey>() ) val request = DeepLinkRequest(uri = "https://www.example.com/users/123") val key = matcher.match(request)?.key // UserProfileKey(id = 123)
Enums
Les valeurs Enum sont mises en correspondance de manière sensible à la casse avec les noms des éléments Enum :
enum class SortOrder { RELEVANCE, DATE, POPULARITY } @Serializable data class ProductsKey(val sort: SortOrder) : NavKey val matcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/products?sort={sort}"), serializer<ProductsKey>() ) val request = DeepLinkRequest(uri = "https://www.example.com/products?sort=DATE") val key = matcher.match(request)?.key // ProductsKey(sort = SortOrder.DATE)
Collections de requêtes répétées
Les paramètres de requête avec des clés répétées (tels que ?id=10&id=20) sont automatiquement
désérialisés en List<T>, Set<T>, ou Array<T>, où T est un type
primitif, String, ou enum :
@Serializable data class FilteredItemsKey(val ids: List<Int>) : NavKey val matcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/items?id={ids}"), serializer<FilteredItemsKey>() ) val request = DeepLinkRequest(uri = "https://www.example.com/items?id=10&id=20") val key = matcher.match(request)?.key // FilteredItemsKey(ids = listOf(10, 20))
Classes @Serializable imbriquées
Lorsqu'un NavKey contient une propriété dont le type est une autre classe @Serializable, UriDeepLinkMatcher aplatit ses propriétés afin que chaque propriété de la classe imbriquée soit directement mappée à un paramètre d'URI individuel du même nom :
enum class SortOrder { RELEVANCE, DATE, POPULARITY } @Serializable data class SearchFilters( val category: String, val sortBy: SortOrder = SortOrder.RELEVANCE ) @Serializable data class SearchKey( val query: String, val page: Int = 1, // Flattened into {category} and {sortBy} val filters: SearchFilters ) : NavKey val matcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/search?q={query}&page={page}&category={category}&sortBy={sortBy}"), serializer<SearchKey>() ) val request = DeepLinkRequest(uri = "https://www.example.com/search?q=kotlin&category=books&sortBy=DATE") val key = matcher.match(request)?.key // SearchKey(query = "kotlin", page = 1, filters = SearchFilters(category = "books", sortBy = SortOrder.DATE))
Sérialisation personnalisée avec DeepLinkSerializer
Pour désérialiser des objets personnalisés (tels que Filter(key = "brand", value =
"pixel")), des types externes (tels que java.time.LocalDate) ou des chaînes délimitées personnalisées (telles que des valeurs séparées par des virgules), étendez DeepLinkSerializer<T>.
DeepLinkSerializer<T> est un KSerializer<T> abstrait qui effectue une conversion
entre un String et un T :
abstract class DeepLinkSerializer<T : Any> : KSerializer<T> {
abstract val serialName: String
abstract fun deserialize(value: String): T
abstract fun serialize(value: T): String
}
Par exemple, considérez les définitions Filter et FilterSerializer utilisées dans les extraits suivants :
@Serializable data class Filter(val key: String, val value: String) object FilterSerializer : DeepLinkSerializer<Filter>() { override val serialName: String = "com.example.Filter" override fun deserialize(value: String): Filter { val parts = value.split(":", limit = 2) if (parts.size < 2) { throw SerializationException("Invalid filter: $value. Expected key:value.") } return Filter(key = parts[0], value = parts[1]) } override fun serialize(value: Filter): String = "${value.key}:${value.value}" }
Objets personnalisés uniques
Pour décoder un objet à partir d'une seule chaîne de paramètres d'URI (telle que ?filter=brand:google), annotez la propriété avec @Serializable(with = ...) :
@Serializable data class CatalogKey( @Serializable(with = FilterSerializer::class) val filter: Filter ) : NavKey val matcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/catalog?filter={filter}"), serializer<CatalogKey>() ) val request = DeepLinkRequest(uri = "https://www.example.com/catalog?filter=brand:google") val key = matcher.match(request)?.key // CatalogKey(filter = Filter("brand", "google"))
Objets personnalisés dans des paramètres de requête répétés
Pour désérialiser des paramètres de requête répétés dans une collection d'objets personnalisés
(List<T>, Set<T>, ou Array<T>), implémentez DeepLinkSerializer<T> pour le
type d'élément T et annotez l'argument de type de la propriété avec
@Serializable(with = ...) :
@Serializable data class SearchResultsKey( val query: String, val filters: List<@Serializable(with = FilterSerializer::class) Filter> = emptyList() ) : NavKey val searchResultsPattern = DeepLinkUri("www.example.com/search?q={query}&filter={filters}") val searchResultsMatcher = UriDeepLinkMatcher(searchResultsPattern, serializer<SearchResultsKey>()) val request = DeepLinkRequest(uri = "https://www.example.com/search?q=phone&filter=brand:google&filter=color:hazel") val matchResult = searchResultsMatcher.match(request) val key = matchResult?.key // SearchResultsKey(query = "phone", filters = listOf(Filter("brand", "google"), Filter("color", "hazel")))
Collections délimitées dans des paramètres uniques
Pour analyser des valeurs séparées par des virgules ou délimitées personnalisées (telles que ?ids=1,2,3) dans une collection, implémentez DeepLinkSerializer pour le type de collection entier et annotez la propriété avec @Serializable(with = ...) :
object IntListCsvSerializer : DeepLinkSerializer<List<Int>>() { override val serialName: String = "com.example.IntListCsv" override fun deserialize(value: String): List<Int> { if (value.isEmpty()) return emptyList() return value.split(",").map { it.trim().toInt() } } override fun serialize(value: List<Int>): String = value.joinToString(",") } @Serializable data class ItemListKey( @Serializable(with = IntListCsvSerializer::class) val ids: List<Int> ) : NavKey val itemListPattern = DeepLinkUri("www.example.com/items/{ids}") val itemListMatcher = UriDeepLinkMatcher(itemListPattern, serializer<ItemListKey>()) val request = DeepLinkRequest(uri = "https://www.example.com/items/10,20,30") val key = itemListMatcher.match(request)?.key // ItemListKey(ids = listOf(10, 20, 30))
Validation des arguments et résultats de correspondance
UriDeepLinkMatcher fait la distinction entre les incohérences (renvoie null afin que d'autres comparateurs puissent être tentés) et les configurations non compatibles (génère une exception).
Incohérences
Une incohérence se produit lorsqu'un URI de la demande entrant ne répond pas aux exigences de modèle ou de type :
- Paramètres obligatoires manquants : propriétés de clé non nullables sans valeurs par défaut dont les paramètres d'URI correspondants sont absents de l'URI de la demande.
- Échecs d'analyse de type : valeurs d'arguments extraites qui ne peuvent pas être analysées
dans le type de propriété attendu (par exemple,
"abc"pour une propriétéInt).
En cas d'incohérence, UriDeepLinkMatcher.match renvoie null, ce qui permet d'évaluer les comparateurs suivants.
Considérez une classe de clé et un comparateur configurés avec des valeurs par défaut, des objets imbriqués et des enums :
enum class MapLayer { STANDARD, SATELLITE, TERRAIN } @Serializable data class LayerOptions( val style: String, val layer: MapLayer = MapLayer.STANDARD ) @Serializable data class MapKey( val location: String, val zoom: Int = 12, val options: LayerOptions ) : NavKey val matcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/map/{location}?zoom={zoom}&style={style}&layer={layer}"), serializer<MapKey>() )
Le tableau suivant illustre les résultats de correspondance pour différents URI de requête :
| URI de la requête | Résultat du décodage | Résultat de la correspondance |
|---|---|---|
https://www.example.com/map/paris?zoom=15&style=dark&layer=SATELLITE |
Opération réussie (tous les paramètres fournis) | UriMatchResult(MapKey("paris", 15, LayerOptions("dark", MapLayer.SATELLITE))) |
https://www.example.com/map/paris?style=dark |
Opération réussie (zoom est défini par défaut sur 12, layer sur STANDARD) |
UriMatchResult(MapKey("paris", 12, LayerOptions("dark", MapLayer.STANDARD))) |
https://www.example.com/map/paris?zoom=&style=dark |
Opération réussie (le paramètre de requête facultatif vide utilise la valeur par défaut 12) |
UriMatchResult(MapKey("paris", 12, LayerOptions("dark", MapLayer.STANDARD))) |
https://www.example.com/map?style=dark |
Incohérence (paramètre location obligatoire manquant) |
null |
https://www.example.com/map/paris?zoom=close&style=dark |
Incohérence ("close" n'est pas un Int) |
null |
https://www.example.com/map/paris?style=dark&layer=HYBRID |
Incohérence ("HYBRID" n'est pas dans l'enum) |
null |
Configurations non compatibles
Si votre classe de clé contient des types de données non compatibles, UriDeepLinkMatcher génère une exception lors de la correspondance au lieu de renvoyer null.
- Maps et collections multidimensionnelles :
UriDeepLinkMatchern'accepte que les collections unidimensionnelles de primitifs, de chaînes, d'enums ou de types personnalisés annotés avec unDeepLinkSerializer.Maptypes throw anIllegalArgumentException, tandis que les collections imbriquées (telles queList<List<String>>) génèrent uneSerializationException. - Collections d'objets personnalisés non annotées : les collections de types personnalisés (telles
que
List<Filter>) génèrent uneSerializationException, sauf si le type d'élément est annoté avec unDeepLinkSerializer. - Classes imbriquées non aplaties : les classes
@Serializablene peuvent pas être mappées à un seul espace réservé (tel que?user={user}) sansDeepLinkSerializer.
// Throws IllegalArgumentException: Map decoding is not supported. @Serializable data class InvalidKey(val tags: Map<String, String>) : NavKey // Throws SerializationException: Only collections of primitives are supported. @Serializable data class InvalidKey(val filters: List<Filter>) : NavKey
Comparaison UriMatchResult
Les instances UriMatchResult sont classées selon les critères suivants, dans l'
ordre :
- Type MatchResult :
UriMatchResultest mieux classé que les autresMatchResulttypes. - Chemin exact : les correspondances de chemin littéral sont mieux classées que les correspondances d'espaces réservés ou de caractères génériques.
- Nombre d'arguments de chemin : les correspondances avec plus d'arguments de chemin sont mieux classées.
- Présence d'arguments : les correspondances qui capturent des arguments sont mieux classées que celles qui n'en capturent pas.
- Nombre total d'arguments : le nombre total d'arguments (chemin, requête, fragment) est le dernier critère de départage.
Personnaliser UriDeepLinkMatcher
UriDeepLinkMatcher est une classe open que vous pouvez sous-classer pour personnaliser le comportement de correspondance d'URI et d'extraction d'arguments :
matchRequest: point d'entrée de correspondance de premier niveau pour unDeepLinkRequestentrant. Remplacez-le pour inspecter les extras de la requête ou appliquer des conditions préalables personnalisées avant la correspondance d'URI.matchUri: fait correspondre leDeepLinkUriau modèle configuré. Remplacez-le pour intercepter et normaliser les URI entrants (par exemple, en réécrivant les sous-domaines dynamiques ou les formats de chemin hérités) avant d'appelersuper.matchUri.matchArguments: désérialise les mappages d'arguments de chemin, de requête et de fragment extraits dans une instance de clé de navigation à l'aide duserializerfourni. Remplacez-le pour injecter des valeurs dynamiques ou transformer des arguments avant l'instanciation de la clé.
L'exemple suivant montre comment sous-classer UriDeepLinkMatcher pour normaliser les préfixes de chemin d'URL hérités avant la correspondance :
class LegacyPrefixUriDeepLinkMatcher<T : Any>( uriPattern: DeepLinkUri, serializer: KSerializer<T> ) : UriDeepLinkMatcher<T>(uriPattern, serializer) { override fun matchUri(uri: DeepLinkUri): UriMatchResult<T>? { val path = uri.path val normalizedUri = if (path != null && path.startsWith("/legacy/")) { DeepLinkUri(uri.toString().replaceFirst("/legacy", "")) } else { uri } return super.matchUri(normalizedUri) } }