階層 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 と 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をスローします。 - アノテーションのないカスタム オブジェクト コレクション: 要素の型が
DeepLinkSerializerでアノテーションされていない場合、カスタム型(List<Filter>など)のコレクションは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 は、サブクラス化して URI の照合と引数の抽出の動作をカスタマイズできる open クラスです。
matchRequest: 受信DeepLinkRequestの最上位のマッチング エントリ ポイント。URI の照合前にリクエストの追加情報を検査したり、カスタムの前提条件を適用したりするには、このメソッドをオーバーライドします。matchUri: 構成されたパターンに対してDeepLinkUriを照合します。super.matchUriを呼び出す前に、このメソッドをオーバーライドして、受信 URI をインターセプトして正規化します(たとえば、動的サブドメインやレガシー パス形式を書き換えます)。matchArguments: 提供されたserializerを使用して、抽出されたパス、クエリ、フラグメント引数マップをナビゲーション キー インスタンスに逆シリアル化します。これをオーバーライドして、キーのインスタンス化の前に動的な値を挿入したり、引数を変換したりします。
次の例は、照合前にレガシー URL パス接頭辞を正規化するために 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) } }