如需将分层 URI 与模式进行匹配并提取实参,请使用 UriDeepLinkMatcher。它依赖于 kotlinx.serialization 将匹配的实参反序列化为您的关键类。
如需创建 UriDeepLinkMatcher,请提供模式 DeepLinkUri 和相应键的序列化程序:
@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")
对于非分层 URI 或自定义架构(例如 tel:),请参阅创建自定义深层链接匹配器。
支持的匹配模式
UriDeepLinkMatcher 根据 URI 的五个组成部分(方案、授权、路径、查询和片段)匹配 URI。以下部分介绍了每个组件支持的模式语法、实参占位符和匹配规则。
方案匹配
如果 URI 格式中没有架构,则同时匹配 http 和 https。
如需匹配特定方案,请将其纳入模式中。不过,如果模式中包含 http 方案,则既匹配 http 请求 URI,也匹配 https 请求 URI;而如果模式中包含 https,则仅匹配 https 请求。
| 模式 URI | 请求 URI | 匹配 |
|---|---|---|
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 |
✅ |
授权匹配
UriDeepLinkMatcher 对 URI 授权(主机和可选端口)执行不区分大小写的完全匹配。授权中不支持占位符或通配符,并且不会提取任何实参:
| 模式 URI | 请求 URI | 匹配 |
|---|---|---|
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 |
❌ |
路径匹配
支持以下路径模式:
| 模式 URI | 请求 URI | 匹配 | 提取的实参 |
|---|---|---|---|
www.example.com/users |
https://www.example.com/users |
✅ | 无 |
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:""(空字符串) |
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 |
✅ | 无 |
www.example.com/users |
https://www.example.com/users/ |
❌(尾部斜杠会创建额外的细分) | 不适用 |
查询匹配
请求 URI 中的查询参数顺序无需与模式 URI 中的顺序一致。此外,系统会忽略请求 URI 中存在但模式 URI 中不存在的参数。
支持以下查询参数模式:
| 模式 URI | 请求 URI | 提取的实参 |
|---|---|---|
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:""(空字符串) |
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" |
fragment 匹配
支持以下 fragment 模式类型:
| 模式 URI | 请求 URI | 提取的实参 |
|---|---|---|
www.example.com/#section1 |
https://www.example.com/#section1 |
无 |
www.example.com/#section_{id} |
https://www.example.com/#section_123 |
id:"123" |
www.example.com/#section_.* |
https://www.example.com/#section_123 |
无 |
受支持的数据类型
UriDeepLinkMatcher 支持将 URI 实参反序列化为原始类型、枚举、集合和自定义对象。序列化分为两类:
- 标准序列化:使用
kotlinx.serialization反序列化为:- 基元(
Boolean、Int、Long、Float、Double、Char、Byte、Short)和String - 枚举
Set、List或Array的原初类型、字符串或枚举- 嵌套的
@Serializable类(其属性会扁平化为各个 URI 占位符)
- 基元(
- 使用
DeepLinkSerializer进行自定义序列化:在单个String与自定义对象、外部类型(例如java.time.LocalDate)或自定义分隔的集合之间进行转换。
标准序列化
UriDeepLinkMatcher 可直接用于标准类型和扁平化结构,无需实现自定义序列化程序。
原始类型和字符串
UriDeepLinkMatcher 会自动解码基元类型(Boolean、Int、Long、Float、Double、Char、Byte、Short)和 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)
枚举
枚举值与枚举元素名称的匹配区分大小写:
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)
重复的查询集合
具有重复键(例如 ?id=10&id=20)的查询参数会自动反序列化为 List<T>、Set<T> 或 Array<T>,其中 T 是原始类型、String 或枚举:
@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))
嵌套的 @Serializable 类
当 NavKey 包含类型为另一个 @Serializable 类的属性时,UriDeepLinkMatcher 会将其属性扁平化,以便嵌套类的每个属性直接映射到同名的各个 URI 参数:
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))
使用 DeepLinkSerializer 进行自定义序列化
如需反序列化自定义对象(例如 Filter(key = "brand", value =
"pixel"))、外部类型(例如 java.time.LocalDate)或自定义分隔字符串(例如以英文逗号分隔的值),请扩展 DeepLinkSerializer<T>。
DeepLinkSerializer<T> 是一个抽象 KSerializer<T>,用于在 String 和 T 之间进行转换:
abstract class DeepLinkSerializer<T : Any> : KSerializer<T> {
abstract val serialName: String
abstract fun deserialize(value: String): T
abstract fun serialize(value: T): String
}
例如,请考虑以下代码段中使用的 Filter 和 FilterSerializer 定义:
@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}" }
单个自定义对象
如需从单个 URI 参数字符串(例如 ?filter=brand:google)解码对象,请使用 @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"))
重复查询参数中的自定义对象
如需将重复的查询参数反序列化为自定义对象 (List<T>、Set<T> 或 Array<T>) 的集合,请为元素类型 T 实现 DeepLinkSerializer<T>,并使用 @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")))
单个参数中的分隔集合
如需将以逗号分隔或以自定义分隔符分隔的值(例如 ?ids=1,2,3)解析为集合,请为整个集合类型实现 DeepLinkSerializer,并使用 @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))
实参验证和匹配结果
UriDeepLinkMatcher 可区分不匹配(返回 null,以便尝试其他匹配器)和不支持的配置(抛出异常)。
不匹配
当传入的请求 URI 不满足模式或类型要求时,就会出现不匹配的情况:
- 缺少必需参数:没有默认值的不可为 null 的键属性,但请求 URI 中缺少相应的 URI 参数。
- 类型解析失败:提取的实参值无法解析为预期属性类型(例如,
Int属性的"abc")。
如果出现不匹配的情况,UriDeepLinkMatcher.match 会返回 null,从而允许评估后续匹配器。
假设有一个使用默认值、嵌套对象和枚举配置的关键类和匹配器:
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>() )
下表展示了各种请求 URI 的匹配结果:
| 请求 URI | 解读结果 | 比赛结果 |
|---|---|---|
https://www.example.com/map/paris?zoom=15&style=dark&layer=SATELLITE |
成功(提供所有参数) | UriMatchResult(MapKey("paris", 15, LayerOptions("dark", MapLayer.SATELLITE))) |
https://www.example.com/map/paris?style=dark |
成功(zoom 默认为 12,layer 默认为 STANDARD) |
UriMatchResult(MapKey("paris", 12, LayerOptions("dark", MapLayer.STANDARD))) |
https://www.example.com/map/paris?zoom=&style=dark |
成功(空的可选查询参数使用默认值 12) |
UriMatchResult(MapKey("paris", 12, LayerOptions("dark", MapLayer.STANDARD))) |
https://www.example.com/map?style=dark |
不匹配(缺少必需的 location 参数) |
null |
https://www.example.com/map/paris?zoom=close&style=dark |
不匹配("close" 不是 Int) |
null |
https://www.example.com/map/paris?style=dark&layer=HYBRID |
不匹配(枚举中没有 "HYBRID") |
null |
不受支持的配置
如果您的键类包含不受支持的数据类型,UriDeepLinkMatcher 会在匹配期间抛出异常,而不是返回 null。
- 映射和多维集合:
UriDeepLinkMatcher仅支持基元、字符串、枚举或带有DeepLinkSerializer注释的自定义类型的单维集合。Map类型会抛出IllegalArgumentException,而嵌套集合(例如List<List<String>>)会抛出SerializationException。 - 未添加注释的自定义对象集合:自定义类型(例如
List<Filter>)的集合会抛出SerializationException,除非元素类型添加了DeepLinkSerializer注释。 - 未扁平化的嵌套类:嵌套的
@Serializable类无法在没有DeepLinkSerializer的情况下映射到单个占位符(例如?user={user})。
// 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
UriMatchResult对比
UriMatchResult 实例按以下标准依次进行排名:
- MatchResult 类型:
UriMatchResult的排名高于其他MatchResult类型。 - 完全匹配路径:字面路径匹配的排名高于占位符或通配符匹配。
- 路径实参数量:具有更多路径实参的匹配项排名更高。
- 实参的存在:捕获实参的匹配项排名高于不捕获实参的匹配项。
- 实参总数:实参(路径、查询、fragment)总数是最终的决胜因素。
自定义UriDeepLinkMatcher
UriDeepLinkMatcher 是一个 open 类,您可以对其进行子类化,以自定义 URI 匹配和实参提取行为:
matchRequest:传入DeepLinkRequest的顶级匹配入口点。替换此方法可在 URI 匹配之前检查请求 extra 或应用自定义前提条件。matchUri:将DeepLinkUri与配置的模式进行匹配。替换此方法以在调用super.matchUri之前拦截并规范化传入的 URI(例如,重写动态子网域或旧版路径格式)。matchArguments:使用提供的serializer将提取的路径、查询和 fragment 实参映射反序列化为导航键实例。替换此方法可在键实例化之前注入动态值或转换实参。
以下示例演示了如何对 UriDeepLinkMatcher 进行子类化,以在匹配之前对旧版网址路径前缀进行规范化:
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) } }