階層 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 は、スキーム、オーソリティ、パス、クエリ、フラグメントの 5 つのコンポーネントに基づいて 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" |
フラグメント マッチング
次のフラグメント パターン タイプがサポートされています。
| パターン 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 引数のプリミティブ型、列挙型、コレクション、カスタム オブジェクトへの逆シリアル化をサポートしています。シリアル化は次の 2 つのカテゴリに分類されます。
- 標準のシリアル化:
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> は、String と T の間で変換を行う抽象 KSerializer<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でアノテーションが付けられたカスタムタイプの 1 次元コレクションのみをサポートします。Map型はIllegalArgumentExceptionをスローし、ネストされたコレクション(List<List<String>>など)はSerializationExceptionをスローします。 - アノテーションのないカスタム オブジェクト コレクション: カスタムタイプ(
List<Filter>など)のコレクションは、要素タイプにDeepLinkSerializerのアノテーションが付けられていない限り、SerializationExceptionをスローします。 - フラット化されていないネストされたクラス: ネストされた
@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タイプよりも上位にランクされます。 - 正確なパス: リテラルパスの一致は、プレースホルダまたはワイルドカードの一致よりも上位にランク付けされます。
- パス引数の数: パス引数が多いほど、一致のランクが高くなります。
- 引数の有無: 引数をキャプチャする一致は、キャプチャしない一致よりも上位にランク付けされます。
- 引数の総数: 引数(パス、クエリ、フラグメント)の総数が最終的なタイブレーカーになります。
UriDeepLinkMatcher をカスタマイズ
UriDeepLinkMatcher は open クラスです。このクラスをサブクラス化して、URI の照合と引数の抽出動作をカスタマイズできます。
matchRequest: 受信DeepLinkRequestの最上位の一致するエントリ ポイント。これをオーバーライドして、URI の照合前にリクエストの追加情報を検査したり、カスタムの前提条件を適用したりします。matchUri:DeepLinkUriを構成済みのパターンと照合します。super.matchUriを呼び出す前に、受信 URI をインターセプトして正規化する(動的サブドメインや以前のパス形式を書き換えるなど)には、これをオーバーライドします。matchArguments: 指定されたserializerを使用して、抽出されたパス、クエリ、フラグメント引数マップをナビゲーション キー インスタンスにデシリアライズします。これをオーバーライドして、動的な値を挿入するか、キーのインスタンス化の前に引数を変換します。
次の例は、UriDeepLinkMatcher をサブクラス化して、照合前に以前の URL パス接頭辞を正規化する方法を示しています。
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) } }