Para hacer coincidir URIs jerárquicos con patrones y extraer argumentos, usa
UriDeepLinkMatcher. Se basa en kotlinx.serialization para deserializar los argumentos coincidentes en tus clases clave.
Para crear un UriDeepLinkMatcher, proporciona un patrón DeepLinkUri y el
serializador para la clave correspondiente:
@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")
Para URIs no jerárquicos o esquemas personalizados (como tel:), consulta
Cómo crear comparadores de vínculos directos personalizados.
Patrones de coincidencia compatibles
UriDeepLinkMatcher hace coincidir los URIs en función de sus cinco componentes: esquema, autoridad, ruta de acceso, consulta y fragmento. En las siguientes secciones, se describen la sintaxis de patrones admitida, los marcadores de posición de argumentos y las reglas de coincidencia para cada componente.
Coincidencia de esquemas
Si no hay un esquema presente en el patrón de URI, coinciden http y https.
Para que coincida con un esquema específico, inclúyelo en el patrón. Como excepción, un
http esquema en un patrón coincide con los URIs de solicitud http y https, mientras que
https en un patrón solo coincide con las solicitudes https.
| URI de patrón | URI de solicitud | Coincidencia |
|---|---|---|
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 |
✅ |
Coincidencia de autoridad
UriDeepLinkMatcher realiza una coincidencia exacta que no distingue mayúsculas de minúsculas en la autoridad del URI (host y puerto opcional). No se admiten marcadores de posición ni comodines en la autoridad, y no se extraen argumentos:
| URI de patrón | URI de solicitud | Coincidencia |
|---|---|---|
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 |
❌ |
Coincidencia de ruta
Se admiten los siguientes patrones de ruta:
| URI de patrón | URI de solicitud | Coincidencia | Argumentos extraídos |
|---|---|---|---|
www.example.com/users |
https://www.example.com/users |
✅ | Ninguno |
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: "" (cadena vacía) |
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 |
✅ | Ninguno |
www.example.com/users |
https://www.example.com/users/ |
❌ (La barra diagonal final crea un segmento adicional) | N/A |
Coincidencia de consultas
El orden de los parámetros de consulta en el URI de solicitud no necesita coincidir con el orden en el URI de patrón. Además, se ignoran los parámetros presentes en el URI de solicitud, pero no en el URI de patrón.
Se admiten los siguientes patrones de parámetros de consulta:
| URI de patrón | URI de solicitud | Argumentos extraídos |
|---|---|---|
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: "" (cadena vacía) |
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" |
Coincidencia de fragmentos
Se admiten los siguientes tipos de patrones de fragmentos:
| URI de patrón | URI de solicitud | Argumentos extraídos |
|---|---|---|
www.example.com/#section1 |
https://www.example.com/#section1 |
Ninguno |
www.example.com/#section_{id} |
https://www.example.com/#section_123 |
id: "123" |
www.example.com/#section_.* |
https://www.example.com/#section_123 |
Ninguno |
Tipos de datos admitidos
UriDeepLinkMatcher admite la deserialización de argumentos de URI en tipos primitivos, enums, colecciones y objetos personalizados. La serialización se divide en dos categorías:
- Serialización estándar: Usa
kotlinx.serializationpara deserializar en lo siguiente:- Primitivas (
Boolean,Int,Long,Float,Double,Char,Byte,Short) yString - Enums
Set,ListoArrayde primitivas, cadenas o enums- Clases
@Serializableanidadas (cuyas propiedades se aplanan en marcadores de posición de URI individuales)
- Primitivas (
- Serialización personalizada con
DeepLinkSerializer: Realiza conversiones entre un soloStringy objetos personalizados, tipos externos (comojava.time.LocalDate) o colecciones delimitadas personalizadas.
Serialización estándar
UriDeepLinkMatcher funciona de inmediato para tipos estándar y estructuras aplanadas sin necesidad de implementaciones de serializador personalizadas.
Primitivas y cadenas
UriDeepLinkMatcher decodifica automáticamente los tipos primitivos (Boolean, Int, Long, Float, Double, Char, Byte, Short) y 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
Los valores de enum se comparan con los nombres de los elementos de enum que distinguen mayúsculas de minúsculas:
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)
Colecciones de consultas repetidas
Los parámetros de consulta con claves repetidas (como ?id=10&id=20) se deserializan automáticamente en List<T>, Set<T>, o Array<T>, donde T es un tipo primitivo, String, o 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))
Clases @Serializable anidadas
Cuando un NavKey contiene una propiedad cuyo tipo es otra clase @Serializable, UriDeepLinkMatcher aplana sus propiedades para que cada propiedad de la clase anidada se asigne directamente a un parámetro de URI individual con el mismo nombre:
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))
Serialización personalizada con DeepLinkSerializer
Para deserializar objetos personalizados (como Filter(key = "brand", value =
"pixel")), tipos externos (como java.time.LocalDate) o cadenas delimitadas personalizadas (como valores separados por comas), extiende DeepLinkSerializer<T>.
DeepLinkSerializer<T> es un KSerializer<T> abstracto que realiza conversiones
entre un String y T:
abstract class DeepLinkSerializer<T : Any> : KSerializer<T> {
abstract val serialName: String
abstract fun deserialize(value: String): T
abstract fun serialize(value: T): String
}
Por ejemplo, considera las definiciones Filter y FilterSerializer que se
usan en los siguientes fragmentos:
@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}" }
Objetos personalizados únicos
Para decodificar un objeto de una sola cadena de parámetros de URI (como ?filter=brand:google), anota la propiedad con @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"))
Objetos personalizados en parámetros de consulta repetidos
Para deserializar parámetros de consulta repetidos en una colección de objetos personalizados
(List<T>, Set<T>, o Array<T>), implementa DeepLinkSerializer<T> para el
tipo de elemento T y anota el argumento de tipo de la propiedad con
@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")))
Colecciones delimitadas en parámetros únicos
Para analizar valores separados por comas o delimitados personalizados (como ?ids=1,2,3) en una colección, implementa DeepLinkSerializer para el tipo de colección completo y anota la propiedad con @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))
Validación de argumentos y resultados coincidentes
UriDeepLinkMatcher distingue entre discrepancias (devuelve null para que se puedan intentar otros comparadores) y parámetros de configuración no compatibles (muestra una excepción).
Discrepancias
Se produce una discrepancia cuando un URI de solicitud entrante no cumple con los requisitos de patrón o tipo:
- Faltan parámetros obligatorios: Propiedades clave que no admiten valores nulos sin valores predeterminados cuyos parámetros de URI correspondientes no están presentes en el URI de solicitud.
- Errores de análisis de tipos: Valores de argumentos extraídos que no se pueden analizar
en el tipo de propiedad esperado (por ejemplo,
"abc"para una propiedadInt).
Cuando se produce una discrepancia, UriDeepLinkMatcher.match devuelve null, lo que permite evaluar los comparadores posteriores.
Considera una clase clave y un comparador configurados con valores predeterminados, objetos anidados y 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>() )
En la siguiente tabla, se muestran los resultados coincidentes para varios URIs de solicitud:
| URI de solicitud | Resultado de la decodificación | Resultado coincidente |
|---|---|---|
https://www.example.com/map/paris?zoom=15&style=dark&layer=SATELLITE |
Listo (se proporcionaron todos los parámetros) | UriMatchResult(MapKey("paris", 15, LayerOptions("dark", MapLayer.SATELLITE))) |
https://www.example.com/map/paris?style=dark |
Listo (zoom tiene el valor predeterminado 12, layer tiene el valor predeterminado STANDARD) |
UriMatchResult(MapKey("paris", 12, LayerOptions("dark", MapLayer.STANDARD))) |
https://www.example.com/map/paris?zoom=&style=dark |
Listo (el parámetro de consulta opcional vacío usa el valor predeterminado 12) |
UriMatchResult(MapKey("paris", 12, LayerOptions("dark", MapLayer.STANDARD))) |
https://www.example.com/map?style=dark |
Discrepancia (falta el parámetro location obligatorio) |
null |
https://www.example.com/map/paris?zoom=close&style=dark |
Discrepancia ("close" no es un Int) |
null |
https://www.example.com/map/paris?style=dark&layer=HYBRID |
Discrepancia ("HYBRID" no está en enum) |
null |
Parámetros de configuración no compatibles
Si tu clase clave contiene tipos de datos no compatibles, UriDeepLinkMatcher muestra una excepción durante la coincidencia en lugar de devolver null.
- Maps y colecciones multidimensionales:
UriDeepLinkMatchersolo admite colecciones unidimensionales de primitivas, cadenas, enums o tipos personalizados anotados con unDeepLinkSerializer. Los tiposMaparrojan unIllegalArgumentException, mientras que las colecciones anidadas (comoList<List<String>>) arrojan unSerializationException. - Colecciones de objetos personalizados sin anotaciones: Las colecciones de tipos personalizados (como
List<Filter>) arrojan unSerializationException, a menos que el tipo de elemento esté anotado con unDeepLinkSerializer. - Clases anidadas sin aplanar: Las clases anidadas
@Serializableno se pueden asignar a un solo marcador de posición (como?user={user}) sin unDeepLinkSerializer.
// 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
Comparación de UriMatchResult
Las instancias UriMatchResult se clasifican según los siguientes criterios en
orden:
- Tipo MatchResult:
UriMatchResulttiene una clasificación más alta que otrosMatchResulttipos. - Ruta de acceso exacta: Las coincidencias de ruta de acceso literales tienen una clasificación más alta que las coincidencias de comodines o marcadores de posición.
- Recuento de argumentos de ruta de acceso: Las coincidencias con más argumentos de ruta de acceso tienen una clasificación más alta.
- Presencia de argumentos: Las coincidencias que capturan argumentos tienen una clasificación más alta que las que no lo hacen.
- Recuento total de argumentos: La cantidad total de argumentos (ruta de acceso, consulta, fragmento) es el factor de desempate final.
Personaliza UriDeepLinkMatcher
UriDeepLinkMatcher es una clase open que puedes crear como subclase para personalizar la coincidencia de URI y el comportamiento de extracción de argumentos:
matchRequest: Es el punto de entrada de coincidencia de nivel superior para unDeepLinkRequestentrante. Anula esto para inspeccionar los extras de la solicitud o aplicar condiciones previas personalizadas antes de la coincidencia de URI.matchUri: Compara elDeepLinkUricon el patrón configurado. Anula esto para interceptar y normalizar los URIs entrantes (por ejemplo, reescribir subdominios dinámicos o formatos de ruta de acceso heredados) antes de llamar asuper.matchUri.matchArguments: Deserializa los mapas de argumentos de ruta de acceso, consulta y fragmento extraídos en una instancia de clave de navegación con elserializerproporcionado. Anula esto para insertar valores dinámicos o transformar argumentos antes de la creación de instancias de clave.
En el siguiente ejemplo, se muestra la creación de subclases de UriDeepLinkMatcher para normalizar los prefijos de ruta de acceso de URL heredados antes de la coincidencia:
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) } }