Для сопоставления иерархических 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 отсутствует схема, будут найдены как http так и https . Чтобы найти конкретную схему, укажите её в шаблоне. Исключение составляет схема http в шаблоне, которая соответствует как http так и https запросам, тогда как 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 | 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> ) реализуйте интерфейс DeepLinkSerializer<T> для элемента типа 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 запроса.
- Ошибки при разборе типов : Извлеченные значения аргументов не могут быть преобразованы в ожидаемый тип свойства (например,
"abc"для свойства типаInt).
При возникновении несоответствия метод 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с заданным шаблоном. Переопределите этот параметр, чтобы перехватывать и нормализовать входящие URI (например, перезаписывать динамические поддомены или устаревшие форматы путей) перед вызовомsuper.matchUri. -
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) } }