Для сопоставления иерархических 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 в шаблоне соответствует URI запросов 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 authority (хост и необязательный порт). Плейсхолдеры и подстановочные знаки в 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 входящего запроса не соответствует требованиям к шаблону или типу:
- Отсутствуют обязательные параметры. Ключевые свойства, которые не могут иметь значение NULL и не имеют значений по умолчанию, а соответствующие им параметры 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 |
Success (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.Maptypes throw anIllegalArgumentException, while nested collections (such asList<List<String>>) throw aSerializationException. - Неаннотированные коллекции пользовательских объектов. Коллекции пользовательских типов (например,
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) } }