כדי להתאים בין מזהי URI היררכיים לתבניות ולחלץ ארגומנטים, משתמשים ב-UriDeepLinkMatcher. הוא מסתמך על kotlinx.serialization כדי לבטל את ההסדרה של ארגומנטים תואמים למחלקות המפתח.
כדי ליצור UriDeepLinkMatcher, צריך לספק תבנית DeepLinkUri ואת ה-serializer של המפתח התואם:
@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 על סמך חמשת הרכיבים שלהם: סכימה, רשות, נתיב, שאילתה ומקטע. בקטעים הבאים מתוארים התחביר של התבניות הנתמכות, ה-placeholder של הארגומנטים וכללי ההתאמה לכל רכיב.
התאמת סכמה
אם אין סכימה בתבנית ה-URI, תהיה התאמה גם ל-http וגם ל-https.
כדי להתאים סכימה ספציפית, צריך לכלול אותה בתבנית. כחריג, סכימת http בתבנית תואמת גם ל-URI של בקשות http וגם ל-URI של בקשות 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 של בקשה | התאמה |
|---|---|---|
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 - טיפוסים בני מנייה (enum)
-
Set,ListאוArrayשל פרימיטיבים, מחרוזות או טיפוסי enum - מחלקות
@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)
ערכי טיפוסים בני מנייה (enum) מותאמים לשמות של רכיבי הטיפוסים בני המנייה (enum) תוך הבחנה בין אותיות רישיות לאותיות קטנות:
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 או enum:
@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:
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" לא נמצא ב-enum) |
null |
הגדרות לא נתמכות
אם מחלקת המפתח מכילה סוגי נתונים לא נתמכים, UriDeepLinkMatcher זורק חריגה במהלך ההתאמה במקום להחזיר null.
- מפות ואוספים רב-ממדיים:
UriDeepLinkMatcherתומך רק באוספים חד-ממדיים של פרימיטיבים, מחרוזות, טיפוסי enum או סוגים מותאמים אישית עם הערות באמצעות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) } }