Pour mettre en correspondance des URI hiérarchiques avec 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 pour 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 (comme tel:), consultez Créer des outils de mise en correspondance de liens profonds personnalisés.
Formats de correspondance acceptés
UriDeepLinkMatcher établit une correspondance avec les URI en fonction de leurs cinq composants : schéma, autorité, chemin, requête et fragment. Les sections suivantes décrivent la syntaxe de format, les espaces réservés d'argument et les règles de correspondance compatibles pour chaque composant.
Correspondance de schéma
Si aucun schéma n'est présent dans le format d'URI, http et https sont mis en correspondance.
Pour faire correspondre un schéma spécifique, incluez-le dans le modèle. Par exception, un schéma http 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 demande | 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 |
✅ |
Mise en correspondance des autorités
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 demande | 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 |
❌ |
Mise en correspondance des chemins d'accès
Les formats de chemin d'accès suivants sont acceptés :
| URI du modèle | URI de la demande | 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 |
Mise en correspondance des requêtes
L'ordre des paramètres de requête dans l'URI de la demande n'a pas besoin de 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 demande | 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 fragments
Les types de modèles de fragment suivants sont acceptés :
| URI du modèle | URI de la demande | 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 accepte la désérialisation des arguments d'URI en types primitifs, en énumérations, en collections et en objets personnalisés. La sérialisation se divise en deux catégories :
- Sérialisation standard : utilise
kotlinx.serializationpour désérialiser dans :- Primitives (
Boolean,Int,Long,Float,Double,Char,Byte,Short) etString - Enums
Set,ListouArrayde primitives, de chaînes ou d'énumérations- Classes
@Serializableimbriquées (dont les propriétés sont aplaties en espaces réservés d'URI individuels)
- Primitives (
- Sérialisation personnalisée avec
DeepLinkSerializer: convertit un seulStringen objets personnalisés, en types externes (tels quejava.time.LocalDate) ou en collections délimitées personnalisées.
Sérialisation standard
UriDeepLinkMatcher fonctionne de manière clé en main pour les types standards et les structures aplaties, sans nécessiter d'implémentations de sérialiseur personnalisées.
Primitives 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 d'énumération sont mises en correspondance avec les noms des éléments d'énumération en tenant compte de la casse :
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 une énumération :
@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 de sorte 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 personnalisées délimitées (telles que des valeurs séparées par une virgule), étendez DeepLinkSerializer<T>.
DeepLinkSerializer<T> est un KSerializer<T> abstrait qui convertit un String en T :
abstract class DeepLinkSerializer<T : Any> : KSerializer<T> {
abstract val serialName: String
abstract fun deserialize(value: String): T
abstract fun serialize(value: T): String
}
Prenons par exemple 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ètre d'URI (par exemple, ?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 les 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 une virgule ou un délimiteur personnalisé (comme ?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 la mise en correspondance
UriDeepLinkMatcher fait la distinction entre les non-correspondances (renvoie null pour que d'autres outils de mise en correspondance puissent être tentés) et les configurations non compatibles (génère une exception).
Incohérences
Une non-concordance se produit lorsqu'un URI de la demande entrant ne répond pas aux exigences de motif 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'argument 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 de non-concordance, UriDeepLinkMatcher.match renvoie null, ce qui permet d'évaluer les outils de mise en correspondance suivants.
Prenons l'exemple d'une classe de clé et d'un outil de mise en correspondance configurés avec des valeurs par défaut, des objets imbriqués et des énumérations :
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 présente les résultats de correspondance pour différents URI de requête :
| URI de la demande | Résultat du décodage | Résultat du match |
|---|---|---|
https://www.example.com/map/paris?zoom=15&style=dark&layer=SATELLITE |
Réussite (tous les paramètres sont fournis) | UriMatchResult(MapKey("paris", 15, LayerOptions("dark", MapLayer.SATELLITE))) |
https://www.example.com/map/paris?style=dark |
Réussite (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 |
Réussite (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" ne figure pas dans l'énumération) |
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 mise en correspondance au lieu de renvoyer null.
- Maps et collections multidimensionnelles :
UriDeepLinkMatchern'accepte que les collections unidimensionnelles de primitives, de chaînes, d'énumérations ou de types personnalisés annotés avec unDeepLinkSerializer. Les typesMapgénèrent une exceptionIllegalArgumentException, tandis que les collections imbriquées (telles queList<List<String>>) génèrent une exceptionSerializationException. - Collections d'objets personnalisés non annotées : les collections de types personnalisés (tels que
List<Filter>) génèrent une exceptionSerializationException, sauf si le type d'élément est annoté avec unDeepLinkSerializer. - Classes imbriquées non aplaties : les classes
@Serializableimbriquées ne 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 de UriMatchResult
Les instances UriMatchResult sont classées selon les critères suivants, dans l'ordre :
- Type MatchResult :
UriMatchResultest mieux classé que les autres typesMatchResult. - Chemin d'accès exact : les correspondances de chemin d'accès littéral sont mieux classées que les correspondances d'espace réservé ou de caractère générique.
- Nombre d'arguments de chemin d'accès : les correspondances avec plus d'arguments de chemin d'accès 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 d'accès, requête, fragment) est le critère de départage final.
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 cette méthode pour inspecter les extras de la requête ou appliquer des conditions préalables personnalisées avant la mise en correspondance des URI.matchUri: compareDeepLinkUriau modèle configuré. Remplacez cette méthode pour intercepter et normaliser les URI entrants (par exemple, en réécrivant les sous-domaines dynamiques ou les anciens formats de chemin d'accè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 de l'serializerfourni. Remplacez cette méthode pour injecter des valeurs dynamiques ou transformer des arguments avant l'instanciation de la clé.
L'exemple suivant montre comment créer une sous-classe de UriDeepLinkMatcher pour normaliser les anciens préfixes de chemin d'URL avant la mise en 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) } }