如要根據模式比對階層式 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 和 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/ |
❌ (尾端斜線會建立額外區隔) | N/A |
查詢比對
要求 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" |
片段比對
系統支援下列片段模式類型:
| 模式 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 不符合模式或類型規定,就會發生不符情況:
- 缺少必要參數:沒有預設值的不可為空鍵屬性,且要求 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類別無法對應至單一預留位置 (例如?user={user}),除非有DeepLinkSerializer。
// 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類型。 - 確切路徑:確切路徑比對的排名高於預留位置或萬用字元比對。
- 路徑引數計數:路徑引數越多,排名越高。
- 引數存在與否:擷取引數的相符項目排名會高於未擷取引數的相符項目。
- 引數總數:引數 (路徑、查詢、片段) 總數是最終的決勝關鍵。
自訂UriDeepLinkMatcher
UriDeepLinkMatcher 是 open 類別,您可以將其加入子類別,自訂 URI 比對和引數擷取行為:
matchRequest:傳入DeepLinkRequest的頂層比對進入點。覆寫此項目,即可檢查要求額外資訊或在 URI 比對前套用自訂前提條件。matchUri:根據設定的模式比對DeepLinkUri。覆寫此項目,即可在呼叫super.matchUri前攔截及正規化傳入的 URI (例如重新編寫動態子網域或舊版路徑格式)。matchArguments:使用提供的serializer,將擷取的路徑、查詢和片段引數對應項還原序列化為導覽鍵例項。覆寫此項目,在鍵例項化之前插入動態值或轉換引數。
以下範例說明如何將 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) } }