如要比對階層式 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/ |
❌ (結尾斜線會建立額外的區隔) | 不適用 |
查詢比對
要求 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註解。 - 未展開的巢狀類別:如果沒有
DeepLinkSerializer,巢狀@Serializable類別就無法對應至單一預留位置 (例如?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類型。 - 精確路徑:常值路徑比對的排名高於預留位置或萬用字元比對。
- 路徑引數數量:路徑引數數量較多的相符項目排名較高。
- 引數存在:擷取引數的比對結果排名高於未擷取的結果。
- 引數總數:引數 (路徑、查詢、片段) 總數是最終的決勝依據。
自訂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) } }