כדי להתאים היררכיות של כתובות 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 על סמך חמשת הרכיבים שלהן: סכימה, סמכות, נתיב, שאילתה ומקטע. בקטעים הבאים מתוארים התחביר הנתמך של התבניות, placeholders של ארגומנטים וכללי ההתאמה לכל רכיב.
התאמת סכמות
אם לא מצוינת סכימה בתבנית ה-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" לא נמצא ברשימת הערכים) |
null |
הגדרות שלא נתמכות
אם המחלקה המרכזית מכילה סוגי נתונים לא נתמכים, הפונקציה UriDeepLinkMatcher מעלה חריגה במהלך ההתאמה במקום להחזיר את הערך null.
- מפות ואוספים רב-ממדיים:
UriDeepLinkMatcherתומך רק באוספים חד-ממדיים של פרימיטיבים, מחרוזות, ספירות או סוגים מותאמים אישית עם הערהDeepLinkSerializer. סוגיםMapיוצריםIllegalArgumentException, ואילו אוספים מקוננים (כמוList<List<String>>) יוצריםSerializationException. - אוספים של אובייקטים מותאמים אישית ללא הערות: אוספים של סוגים מותאמים אישית (כמו
List<Filter>) מקפיצים הודעת שגיאה (throw)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מקבל דירוג גבוה יותר מסוגים אחרים שלMatchResultMatchResult. - נתיב מדויק: התאמות מדויקות של נתיבים מקבלות דירוג גבוה יותר מהתאמות של placeholder או wildcard.
- מספר הארגומנטים של הנתיב: התאמות עם יותר ארגומנטים של נתיב מקבלות דירוג גבוה יותר.
- נוכחות של ארגומנטים: התאמות שכוללות ארגומנטים מדורגות גבוה יותר מהתאמות שלא כוללות ארגומנטים.
- המספר הכולל של הארגומנטים: המספר הכולל של הארגומנטים (נתיב, שאילתה, מקטע) הוא הקריטריון האחרון להכרעה.
התאמה אישית של UriDeepLinkMatcher
UriDeepLinkMatcher הוא מחלקה מסוג open שאפשר ליצור ממנה מחלקת משנה כדי להתאים אישית את ההתנהגות של התאמת URI וחילוץ ארגומנטים:
-
matchRequest: נקודת כניסה להתאמה ברמה העליונה שלDeepLinkRequestנכנס. אפשר לשנות את ההגדרה הזו כדי לבדוק את התוספים של הבקשה או להחיל תנאים מוקדמים מותאמים אישית לפני התאמת ה-URI. -
matchUri: התאמה שלDeepLinkUriלתבנית שהוגדרה. אפשר להגדיר חריגה כדי ליירט ולנרמל כתובות URI נכנסות (לדוגמה, לשכתב תת-דומיינים דינמיים או פורמטים של נתיבים מדור קודם) לפני שמפעילים אתsuper.matchUri. -
matchArguments: מבצעת דה-סריאליזציה של מפות הארגומנטים של הנתיב, השאילתה והמקטע שחולצו למופע של מפתח ניווט באמצעותserializerשסופק. אפשר לשנות את ההגדרה הזו כדי להוסיף ערכים דינמיים או לשנות את הארגומנטים לפני יצירת המופע של המפתח.
בדוגמה הבאה מוצג שימוש ב-subclassing של 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) } }