מהנדס תוכנה
ניהול קישורי עומק באפליקציות מודולריות ל-Android יכול בקלות להוביל לנתיבים לא תקינים ולהפתעות בזמן הריצה.בסשן הזה נסביר על הארכיטקטורה החדשה של Jetpack Navigation3 לקישורי עומק עם מניעת שגיאות הקלדה ב-Compose, כדי לעזור לכם ליצור מערכת ניתוב חזקה וניתנת להתאמה. נלמד איך להטמיע ממשקי API מוכנים לשימוש להתאמת קישורי עומק, לכתוב סריאליזטורים בהתאמה אישית לארגומנטים מורכבים למניעת שגיאות הקלדה, וליצור מקבצי פעילויות קודמות (back stack) היררכיים ודינמיים ששומרים על הניווט של המשתמשים צפוי.
DeepLinkRequest שתואמת ל-KMP כוללת את הקלט, כולל בנאי Intent של Android.UriDeepLinkMatcher משתמש בסריאליזציה של Kotlin כדי להתאים לתבניות URI ולבטל את הסריאליזציה של ארגומנטים למאפייני NavKey עם מניעת שגיאות הקלדה. סוגים פרימיטיביים פועלים באופן אוטומטי, וההתאמות נכשלות אם לא ניתן לנתח את הערכים.DeepLinkSerializer או לשנות את matchArguments כדי לטפל במחרוזות מורכבות של קישורי עומק. יצירת מחלקת משנה של UriDeepLinkMatcher מאפשרת לכם לעבד מראש פלייסהולדרים של URI גולמי לפני ההפעלה של Kotlin Serialization.withBackStack על התאמות יוצרת מקבצי פעילויות קודמות (back stack) סינתטיים באופן עצל להתאמה הטובה ביותר. כך אפשר להבטיח שהמערכת תתנהג כמו שצריך כשחוזרים אחורה, בלי לחשב מקבצי פעילויות קודמות מיותרים לכל התאמה חוקית אחרת.תמליל
שלום, אני איאן לייק, מנהל ההנדסה של צוות Jetpack Architecture.
ואני קלרה פוק, המובילה הטכנית של Nav 3.
והיום אנחנו כאן כדי לדבר על תכונה שקרובה לליבי, נב – קישורי עומק.
למעשה, אני כל כך אוהב אותם שהטמעתי גם את Nav2 וגם את Nav3 כחלק מגרסה 1.2 של Nav3.
אני חושב שבעצם עשית את זה שלוש פעמים, כי הטמענו את זה גם במתכונים של Nav 3.
כן, זה נכון.
עברנו הרבה איטרציות ב-API כדי לוודא שהוא מתאים לגישה של Nav3 שמתמקדת ב-Compose.
הרבה מזה התחיל עם הדגש שלנו על מתכונים.
כן, אחת המטרות העיקריות שלנו ב-Navigation 3 הייתה לא להפריע לכם.
אם אתם רוצים לפתח תכונה שאפתנית במיוחד, אתם לא צריכים לשלוח לנו הגשת בקשה להוספת תכונה כדי שזה יתאפשר.
אז כששואלים אותנו "איך עושים את זה?",
או "איך בכלל ניגשים לבעיה הזו?"
במקום ליצור API בהתאמה אישית כבר מההתחלה, אנחנו יוצרים מתכון – מדריך שמסביר איך אפשר להוסיף את התכונה הזו לאפליקציה בלי בהכרח לבצע שינויים ב-Navigation 3.
הסגנון הזה של "להראות במקום לספר" הוביל אותנו ליצור את מאגר המתכונים של Nav3 ב-GitHub.
עכשיו יש בו יותר מ-35 מתכונים לתרחישים נפוצים לדוגמה, כמו כמה מקבצי פעילויות קודמות (back stack) וגיליונות תחתונים, שמספקים את כל הקוד שצריך כדי להטמיע אותם באפליקציה.
זה אומר שכבר בגרסה 1.0 של Nav3, שיצאה בנובמבר 2025, היה לנו מתכון להטמעת קישורי עומק כי ידענו שזה חשוב להרבה מכם.
אבל כשאנחנו מבינים שמתכון דורש הרבה שיטות עזר שצריך להעתיק ולהדביק באפליקציה, אנחנו שוקלים תהליך שנקרא "סיום", שבו אנחנו לוקחים את שיטות העזר האלה ואומרים, "אוקיי, איך זה היה נראה אם הן היו חלק מ-Navigation 3 עצמו?"
ככה, אם מפתחים כבר העתיקו והדביקו את הקוד הזה, זה בסדר.
אתם יכולים להשאיר את הקוד הזה.
אם יש לכם פתרונות בהתאמה אישית שעובדים, זה מצוין.
אין לך העברה כפויה.
אבל אם זו בעיה חדשה לגמרי או דרישה חדשה, אתם מקבלים פתרון קל ומאומת שמאפשר לכם להשתמש בו ישירות כחלק מהניווט.
אבל החלק הכי טוב, בשבילי באופן אישי, הוא שאחרי שאנחנו מוסיפים את פונקציית העזר הזו, אנחנו חוזרים ומעדכנים את המתכונים שלנו ומקבלים את הדופמין המתוק הזה של מחיקת קוד.
כך, כל מי שמעיין במתכונים כדי למצוא את הגישה המומלצת לבעיה הזו, מקבל את הפתרון העדכני ביותר.
כמובן, בדיעבד קל להיות חכם, ותהליך הסיום הזה נותן לנו הזדמנות לבחון באמת את כלי העזר האלה שבנינו במתכונים ולומר, "האם אלה באמת ממשקי ה-API הטובים ביותר בשביל זה?"
ובמקרה הזה, זה נותן לנו הזדמנות לשפר את הדברים האלה כשאנחנו מעבירים אותם לספרייה.
וזה בהחלט היה המצב עם קישורי עומק.
תודה, איאן.
אז מבחינה רעיונית, גם המתכונים וגם ממשקי ה-API הסופיים שלנו דומים מאוד, כי ב-Nav 3, מקבץ הפעילויות הקודמות (back stack) נמצא בבעלותכם.
זה אומר שהמטרה של ממשקי ה-API שלנו לקישורי עומק היא להבין מה צריך להיות מקבץ הפעילויות הקודמות הראשוני, ובאופן ספציפי, איזה מסך המשתמשים רואים כשהם לוחצים על קישור עומק כדי להיכנס לאפליקציה.
אבל מה קורה בפועל כשהמשתמש לוחץ על ה-URI הזה?
עכשיו, גם אם אתם משתמשים ב-Nav3 וגם אם לא, מסגרת Android דורשת שתצהירו על מסנני Intent לכל קישור עומק שהאפליקציה שלכם תומכת בו.
עכשיו, החלק המעניין הוא מה שקורה אחרי שקישור העומק מפעיל את ה-Activity.
כאן, הקלט הוא ה-Intent שאיתו האפליקציה מתחילה, ה-Activity שהתחיל.
אבל קישור העומק יכול להופיע בהרבה צורות.
לדוגמה, יכול להיות שתרצו ליצור אותו באופן ידני לצורך בדיקה, או לנצל את התמיכה של Kotlin Multiplatform.
לכן, כללנו את הקלט של קישור העומק במחלקת DeepLinkRequest שתואמת ל-KMP.
וב-Android, יש לנו בנאי DeepLinkRequest שמקבל Intent של Android.
המטרה הבאה שלנו היא להבין, בהינתן בקשה, איך אנחנו יודעים מה NavKey צריך להכניס למקבץ הפעילויות הקודמות (back stack)?
כאן המחלקה DeepLinkMatcher נכנסת לתמונה.
היא מקבלת בקשה ומוציאה MatchResult, שמכיל את ה-NavKey שמשויך להתאמה.
במיוחד עבור מזהי URI באינטרנט, יש לנו UriDeepLinkMatcher שמכיל את כל הלוגיקה שנדרשת כדי להפוך בקשה נתונה ל-MatchResult.
val matcher = UriDeepLinkMatcher(
...,
...
)עכשיו, הפרמטר הראשון ש-UriDeepLinkMatcher מקבל הוא תבנית URI.
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri(...),
...
)נניח שאתם מפתחים אפליקציה ל-Android, ואתם רוצים להוסיף קישור עומק למסך הבית.
כאן, התבנית היא URI מדויק, ואנחנו יכולים להשמיט את סכימת ה-HTTP כדי להתאים באופן אוטומטי גם ל-HTTP וגם ל-HTTPS.
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/home"),
...
)אבל אם אנחנו רוצים ליצור קישור עומק לביקור מסוים עם חשיפה למודעות, קישור העומק הזה מכיל מידע שאנחנו צריכים לחלץ, כלומר מזהה הביקור עם חשיפה למודעות.
// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
…
)לכן, במקרה הזה, התבנית שלנו משתמשת בסוגריים מסולסלים כפלייסהולדרים למידע שאנחנו צריכים לחלץ מקישור העומק.
מסתבר שכבר יש ספרייה שימושית להמרת כל סוג של קלט למופעי מחלקה, שנקראת Kotlin Serialization.
לכן, ה-UriDeepLinkMatcher שלנו משתמש במפענח Kotlin כדי לבטל את ההמרה של URI של קלט ולהפיק מופע של NavKey.
// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
serializer = serializer<Session>()
)עכשיו, המפענח הזה צריך לדעת עם איזו מחלקה לעבוד, אז צריך גם להעביר ל-matcher את ה-serializer של NavKey.
// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
serializer = serializer<Session>()
)כדי לעזור למפענח להתאים בצורה נכונה בין הפלייסהולדרים למאפיינים של המפתח, השם של הפלייסהולדר צריך להיות זהה לשמות המאפיינים.
לכן בדוגמה הזו, שם המאפיין הוא מזהה שתואם לפרמטר המזהה של הסשן.
אוקיי, עכשיו כשיש לנו את UriDeepLinkMatcher, אנחנו צריכים ליצור DeepLinkRequest עם קישור העומק הנכנס.
// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
serializer = serializer<Session>()
)
val request = DeepLinkRequest(
uri = DeepLinkUri("http://ads.android.com/session/abcd")
)
val matchResult: UriMatchResult<Session>? = matcher.match(request)
val backStack = listOf(matchResult?.key ?: Home)לאחר מכן, ההתאמה מתבצעת על ידי קריאה לפונקציית ההתאמה והעברת הבקשה שלנו.
כך תקבלו MatchResult שניתן להגדיר כ-null ומכיל את ה-NavKey, שבדוגמה שלנו הוא מפתח להצפנת פעילות. אם התוצאה היא null, אפשר פשוט להגדיר את מסך הבית כברירת מחדל.
עכשיו, צריך לכתוב כמה קישורי עומק בתור כמה התאמות: התאמה אחת לכל קישור עומק.
לדוגמה, כאן יש לנו שתי תבניות URI למפתח להצפנת פעילות: אחת לתבנית ניוזלטר מדור קודם ואחת ל-URI רגיל באינטרנט.
ויש לנו גם קישור עומק למסך הבית של ADS, אז אנחנו מוסיפים התאמה שלישית בשבילו.
וכל ההתאמות יתווספו לאותה רשימה שאנחנו עוברים עליה.
אז עכשיו ההתאמה שלנו הופכת ללולאה על כל ההתאמות ברשימה שלנו, שבה אנחנו קוראים להתאמה בכל אחת מהן כדי למצוא את כל ההתאמות שתואמות לבקשה.
כדי לעשות את זה, אנחנו יכולים להשתמש ב-mapNotNull כדי לסנן את מה שלא תואם ולקבל רשימה של ההתאמות התקינות בלבד.
val matchers = listOf(...)
// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
matcher.match(request)
}ברוב המקרים, כנראה תהיה רק התאמה אחת שאינה null ברשימה, אבל יש מקרים שבהם יכולה להיות יותר מהתאמה חוקית אחת.
לדוגמה, נניח שאנחנו מציגים מסך מיוחד של הרצאה מרכזית שמשתמש באותה תבנית URI של סשן, אבל עכשיו במקום ה-placeholder של המזהה, יש לו נתיב סטטי של הרצאה מרכזית.
לכן, במקרה הזה, אותה בקשה יכולה להתאים לשני הכללים: המפתח להצפנת פעילות שבו המזהה שווה ל-keynote ו-NavKey של keynote.
אז מכיוון ש-MatchResult מיישם את Comparable, אנחנו יכולים למצוא את ההתאמה הכי טובה על ידי השוואה בין כל ההתאמות החוקיות ברשימה באמצעות הפונקציה max, או בעצם maxOrNull כדי לטפל במקרה שבו לא היו קישורי עומק שתואמים לבקשה.
val matchers = listOf(...)
// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
matcher.match(request)
}
// compare matches
val backStack = listOf(
matchResults.maxOrNull()?.key ?: Home
)רשימה סטטית של התאמות יכולה להתאים להרצאת בזק, אבל כמו שהמסכים יכולים להתפרס על פני כמה מודולים, אנחנו מצפים שאותם מודולים יתרמו לרשימה גלובלית של התאמות דרך מערכת ה-DI.
// Feature A Module
@ContributesTo(ActivityScope::class)
@BindingContainer
object FeatureAModule {
@IntoSet
@Provides
fun provideDeepLinkMatcher() : UriDeepLinkMatcher<NavKey> =
UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/featureA"),
serializer<FeatureBKey>(),
)
}
// in MainActivity
@Inject lateinit var matchers: Set<UriDeepLinkMatcher<NavKey>>בגישה הזו, המתכונים של Hilt, Metro ו-Koin במאגר המתכונים שלנו עודכנו כך שיכללו קישורי עומק.
קלרה ציינה שה-UriDeepLinkMatcher שלנו משתמש ב-Kotlin Serialization כדי לחלץ שדות מבקשת קישור העומק ולהפוך אותה למחלקה שלנו למניעת שגיאות הקלדה, כמו מחלקת הסשן שהראינו.
כמו שכתוב כאן, זו טרנספורמציה די פשוטה.
ה-URI הוא מחרוזת.
השדה הוא מחרוזת.
וואו, זה היה קל.
אבל ידענו שאנחנו צריכים להרחיב את האפשרויות מעבר למאפייני מחרוזת.
כאן Kotlin Serialization כבר נותנת לנו בסיס יציב לעבוד איתו.
כל הסוגים הפרימיטיביים, כמו בוליאנים, מספרים שלמים, מספרים ממשיים ואפילו Enums, פועלים באופן מיידי. אם אי אפשר לנתח את ערך הקישור העמוק כסוג הספציפי הזה, ההתאמה נכשלת באופן אוטומטי ומוחזר ערך null.
לדוגמה, אם פרמטר המזהה של הסשן היה מספר שלם במקום מחרוזת, העברת מחרוזת שלא ניתן לנתח כמספר שלם הייתה גורמת לכך שההתאמה תיכשל.
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKeyבאופן דומה, אם אתם רוצים להוסיף ארגומנטים אופציונליים, למשל לנתוני ניתוח שמצורפים לכתובת ה-URL, הם יכולים להיות מאפיינים במפתח עם ערכי ברירת מחדל.
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
val id: Int,
val referrer: String? = null,
) : NavKeyכאן אפשר לראות שניסינו מאוד להפריד בין המבנה של כתובת ה-URL, שיכול להיות ששייכת לצוות אחר, לבין המחלקות למניעת שגיאות הקלדה או המחלקות שמייצגות את ההתאמה של קישור העומק באפליקציה.
כי אני יודע שחלק מכם מתמודדים עם קישורי עומק ממש גרועים.
// "id={id}&base64={placeholderImage}"
@Serializable data class Session(
val id: String,
val placeholderImage: Bitmap? = null
) : NavKey
// "jsonBlob={user}"
@Serializable data class Profile(
val user: User,
) : NavKeyלמשל, אם יש לכם תמונה בקידוד Base64 שמצורפת לקישור העומק.
למשל, אולי יש קובץ JSON ענק שהם החליטו, "טוב, בואו פשוט נכניס את זה לכל כתובת URL של האפליקציה".
עכשיו יש דרך לעשות את זה, גם אם אתם לא יכולים לשנות את כתובת ה-URL שקיבלתם, אבל אתם עדיין צריכים לחלץ ממנה מידע מסוים.
אז השיטה הראשונה היא להרחיב את המחלקה DeepLinkSerializer כדי לשלוט בדיוק באופן שבו הארגומנט הזה עובר דה-סריאליזציה או המרה ממחרוזת למאפיין מניעת שגיאות ההקלדה.
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
}DeepLinkSerializer הוא wrapper של Kotlin Serialization שמפשט את ההטמעה של מחלקת ה-serializer, כי במקרה הזה, הקלט של ה-deserializer הוא תמיד מחרוזת מכתובת ה-URL.
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>כדי להטמיע DeepLinkSerializer, צריך לכתוב שלוש שיטות.
קודם בוחרים שם לסדרה.
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
override val serialName: String
get() = "Bitmap"
...
}לא צריך להגדיר מרחב שמות, אבל הוא צריך להיות ייחודי מספיק כדי ש-Kotlin Serialization ידע מתי להשתמש ב-serializer.
בשלב הבא, צריך להטמיע את deserialize, שמועבר למחרוזת הזו מכתובת ה-URL, והוא אחראי ליצירת האובייקט למניעת שגיאות הקלדה.
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
override val serialName: String
get() = "Bitmap"
override fun deserialize(value: String): Bitmap {
val imageBytes = Base64.decode(value, Base64.DEFAULT)
// This better be a small image…
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
...
}לבסוף, תטמיעו את serialize, שהיא הגישה ההפוכה, ומשתמשים בה אם אותה מחלקת מפתח משמשת במקרים כמו rememberNavBackStack.
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
override fun serialize(value: Bitmap): String {
val byteArrayOutputStream = ByteArrayOutputStream()
// Compress the bitmap into the output stream (e.g., JPEG or PNG)
value.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
val byteArray = byteArrayOutputStream.toByteArray()
// Encode the byte array to a Base64 string
return Base64.encodeToString(byteArray, Base64.DEFAULT)
}
}עכשיו צריך להחיל את ה-serializer המותאם אישית על המאפיין שיש לו את המחלקה המיוחדת הזו, ו-web UriDeepLinkMatcher יטפל בכל השאר.
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
}
@Serializable data class Session(
val id: String,
@Serializable(BitmapSerializer::class)
val placeholderImage: Bitmap? = null
) : NavKeyאבל מה אם אתם רוצים רק חלק קטן מהנתונים ממחרוזת Base64 או מ-JSON שצורף לכתובת ה-URL?
במקרה כזה, יכול להיות שכדאי לעבד מראש חלק מכתובת ה-URL לפני שהיא מגיעה ל-Kotlin Serialization.
כדי לטפל במקרים האלה, אפשר להרחיב את UriDeepLinkMatcher ולבטל את השיטה matchArguments שלו.
עכשיו, אולי עבור ה-JSON blob שלנו, כל מה שאנחנו באמת רוצים זה רק את מזהה הסשן הזה, כאילו לא ממש אכפת לנו מכל השאר.
אז אנחנו יוצרים את מחלקת המשנה שלנו ומבטלים את matchArguments.
class SessionUriDeepLinkMatcher(
uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {
override fun matchArguments(
pathArgs: Map<String, List<String>>,
queryArgs: Map<String, List<String>>,
fragmentArgs: Map<String, List<String>>
): UriMatchResult<Session>? {
...
}הקלט הוא המחרוזות הגולמיות האלה, כל ה-placeholders שנוספו מנתיב ה-URI.
זה מאפשר לנו לעבד מראש ולערוך את הערכים האלה לפני שאנחנו קוראים ל-super.matchArguments.
class SessionUriDeepLinkMatcher(
uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {
override fun matchArguments(
pathArgs: Map<String, List<String>>,
queryArgs: Map<String, List<String>>,
fragmentArgs: Map<String, List<String>>
): UriMatchResult<Session>? {
val finalQueryArgs = buildMap {
putAll(queryArgs)
val jsonBlob = remove("session")?.firstOrNull()
if (jsonBlob != null) { put("session", extractSessionIdFromJson(jsonBlob)) }
}
return super.matchArguments(pathArgs, finalQueryArgs, fragmentArgs)
}עכשיו אנחנו יכולים להשתמש בערכים פשוטים יותר, ולא צריך להתעסק בכלל עם Kotlin Serialization.
תודה, איאן.
אנחנו מקווים שלא תצטרכו להשתמש בשיטות האלה לעיתים קרובות, אבל השקענו הרבה מחשבה כדי לאפשר לכם לשלוט בהתנהגות של ההתאמה.
אוקיי.
עכשיו אנחנו יודעים איך להגיע אל NavKey באמצעות ההתאמות של קישורי העומק, אבל יכול להיות שמקבץ פעילויות קודמות (back stack) עם מפתח הניווט בלבד לא יספק את חוויית המשתמש הטובה ביותר.
לכן, כדי להבטיח שהחזרה אחורה תפעל כמו שצריך, כדאי להשתמש במקבץ פעילויות קודמות סינתטי.
החלק הסינתטי נובע מכך שמקבץ הפעילויות הקודמות מכיל מפתחות שהמשתמש אף פעם לא ניווט אליהם באופן ידני, אבל זה הנתיב שהוא היה עובר בו אם הוא היה מנווט לשם באופן ידני.
לדוגמה, אם המשתמש לוחץ על התראה לגבי סשן שעומד להתחיל, אתם יכולים להעביר אותו ישירות לסשן הזה. אבל כשהוא לוחץ על כפתור החזרה של המערכת, כנראה תרצו שהוא יחזור למסך הבית שלכם לפני שהוא יוצא למרכז האפליקציות.
מסך הבית הזה הופך לחלק ממקבץ הפעילויות הקודמות הסינתטי.
בממשקי API של קישורי עומק, רצינו לאזן בין שתי דרישות מתחרות.
מצד אחד, אנחנו יודעים שקישורי עומק שונים, או מתאמים שונים, ירצו לוגיקה שונה לבניית מקבץ הפעילויות הקודמות הסינתטי שלהם.
לדוגמה, הוספה של HomeScreen לפני כל קישור עומק יכולה לעבוד עד שרוצים להוסיף קישור עומק למסך משנה בתוך מסך הגדרות.
עכשיו, אתם צריכים שכבה נוספת במקבץ הפעילויות הקודמות (back stack) הסינתטי שלכם בשביל קישור העומק הספציפי הזה.
באופן טבעי, זה מקשר את ה-back stack builder לכל התאמה של קישור עומק.
מצד שני, אנחנו גם יודעים שכל אלפית שנייה שעוברת עד שהמסך הנכון מוצג עולה לכם כסף.
לכן לא רצינו לחשב מקבץ פעילויות קודמות סינתטי לכל תוצאת התאמה.
רצינו רק את ההתאמה הכי טובה.
אז אם אנחנו רוצים לוגיקה מותאמת אישית לכל התאמה, אבל אנחנו לא רוצים ליצור מקבץ פעילויות קודמות (back stack) סינתטי באופן מיידי לכל התאמה, מה אנחנו עושים?
אחרי כמה ניסיונות כושלים, מצאנו גישה שמאזנת בין שתי הדרישות.
לא משנה איזה סוג של Matcher יש לכם, אתם יכולים להרחיב את ה-Matcher הספציפי הזה על ידי קריאה ל-withBackStack כדי ליצור באופן עצל את מקבץ הפעילויות הקודמות עם ה-MatchResult הנתון.
val homeMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
listOf(Home, matchResult.key)
}
val sessionMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
listOf(Home, SessionList, matchResult.key)
}
val backStackMatchers: List<BackStackMatcher<NavKey, NavKey>> = listOf(
homeMatcher, sessionMatcher
)
val matchResults = backStackMatchers.mapNotNull { matcher.match(request) }
val bestMatch: BackStackMatchResult<NavKey, NavKey>? = matchResults.maxOrNull()
val backStack = bestMatch?.backStack ?: listOf(Home)לכן, אם יש לנו כמה התאמות, לכל אחת מהן תהיה קריאה משלה ל-withBackStack כדי ליצור את מקבץ הפעילויות הקודמות (back stack) המתאים ל-NavKey הספציפי הזה.
כדי לקבל את הקוד הנקי ביותר, כל פונקציית התאמה שאתם קוראים לה צריכה להשתמש ב-withBackStack, שתיתן לכם רשימה של פונקציות התאמה של מקבץ פעילויות קודמות (back stack).
וכשמוצאים את ההתאמה הכי טובה, התוצאה שמתקבלת היא BackStackMatchResult.
זוהי תת-מחלקה של MatchResult שמוסיפה מאפיין חדש בשם backStack, שהוא כל מקבץ הפעילויות הקודמות שנוצר על ידי פונקציית הלמבדה withBackStack.
הגישה הזו של שכבות אפשרה לנו להפריד בין הלוגיקה בתוך ההתאמות לבין כל לוגיקה מותאמת אישית שרוצים להשתמש בה בתוך פונקציית הלמבדה של withBackStack.
זו הרצאת בזק, אז לצערנו הזמן שלנו כמעט נגמר, אבל אולי נוכל לעבור על כמה מהתכונות האחרות שיש לנו ב-Navigation 3, שאפשר למצוא במתכונים של Navigation 3.
אוקיי.
לכן, אתם יכולים להוסיף מסננים לכל התאמה כדי לקצר את הלוגיקה של ההתאמה ולהחריג במהירות את ההתאמה על סמך פעולה, סוג MIME או מסננים מותאמים אישית משלכם.
וזה קורה לפני כל ניתוח URI או סריאליזציה בהתאמה אישית, סריאליזציה של Kotlin.
אנחנו מציעים גם StaticKeyDeepLinkMatcher, במיוחד למקרים שבהם לא צריך לבצע ניתוח כדי לקבל את ה-NavKey, אבל עדיין רוצים להחיל מסננים.
@Serializable object ImagesKey: NavKey
// custom filter
val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))האפשרות הזו מושלמת לטיפול בקישורי עומק מסוג פעולה או MIME בלבד, כמו אפליקציה לעריכת תמונות שיכולה לפתוח תמונות JPEG.
ואם כבר מדברים על פעולה וסוג MIME, אתם יכולים לצרף כל RequestExtras מותאם אישית ל-DeepLinkRequest שלכם, והמסננים המותאמים אישית ומחלקות המשנה של DeepLinkRequest יכולים להשתמש בו כחלק מהלוגיקה שלהם.
כך אפשר לשלב מסגרות של בדיקות AB או אותות חיצוניים.
ויש לנו גם ממשקי API שלמים למניעת שגיאות הקלדה לתוספות.
val extras = requestExtras {
put(SectionExtrasKey, "A")
}אה, וכמעט כל הלוגיקה הזו נמצאת בקוד ה-KMP הנפוץ של Nav3, כך שאפשר לשתף את כל ההתאמות והלוגיקה של מקבץ הפעילויות הקודמות (back stack) בין פלטפורמות.
אם אתם רוצים לראות איך Navigation 3 תומך בקישורי עומק, כדאי לכם לעיין במתכונים של Nav3.
כן, וגם שילבנו את ממשקי ה-API של קישורי העומק במתכוני ה-DI שלנו, וגם באפליקציה המובילה שלנו, NowInAndroid.
תודה שהצטרפתם להרצאת הבזק שלנו, ובהצלחה בניווט.
תודה.
val matcher = UriDeepLinkMatcher(
...,
...
)val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri(...),
...
)val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/home"),
...
)// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
…
)// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
serializer = serializer<Session>()
)// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
serializer = serializer<Session>()
)// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
serializer = serializer<Session>()
)
val request = DeepLinkRequest(
uri = DeepLinkUri("http://ads.android.com/session/abcd")
)
val matchResult: UriMatchResult<Session>? = matcher.match(request)
val backStack = listOf(matchResult?.key ?: Home)val matchers = listOf(...)
// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
matcher.match(request)
}val matchers = listOf(...)
// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
matcher.match(request)
}
// compare matches
val backStack = listOf(
matchResults.maxOrNull()?.key ?: Home
)// Feature A Module
@ContributesTo(ActivityScope::class)
@BindingContainer
object FeatureAModule {
@IntoSet
@Provides
fun provideDeepLinkMatcher() : UriDeepLinkMatcher<NavKey> =
UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/featureA"),
serializer<FeatureBKey>(),
)
}
// in MainActivity
@Inject lateinit var matchers: Set<UriDeepLinkMatcher<NavKey>>// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
val id: Int,
val referrer: String? = null,
) : NavKey// "id={id}&base64={placeholderImage}"
@Serializable data class Session(
val id: String,
val placeholderImage: Bitmap? = null
) : NavKey
// "jsonBlob={user}"
@Serializable data class Profile(
val user: User,
) : NavKeyobject BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
}public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
override val serialName: String
get() = "Bitmap"
...
}object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
override val serialName: String
get() = "Bitmap"
override fun deserialize(value: String): Bitmap {
val imageBytes = Base64.decode(value, Base64.DEFAULT)
// This better be a small image…
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
...
}object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
override fun serialize(value: Bitmap): String {
val byteArrayOutputStream = ByteArrayOutputStream()
// Compress the bitmap into the output stream (e.g., JPEG or PNG)
value.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
val byteArray = byteArrayOutputStream.toByteArray()
// Encode the byte array to a Base64 string
return Base64.encodeToString(byteArray, Base64.DEFAULT)
}
}object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
}
@Serializable data class Session(
val id: String,
@Serializable(BitmapSerializer::class)
val placeholderImage: Bitmap? = null
) : NavKeyclass SessionUriDeepLinkMatcher(
uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {
override fun matchArguments(
pathArgs: Map<String, List<String>>,
queryArgs: Map<String, List<String>>,
fragmentArgs: Map<String, List<String>>
): UriMatchResult<Session>? {
...
}class SessionUriDeepLinkMatcher(
uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {
override fun matchArguments(
pathArgs: Map<String, List<String>>,
queryArgs: Map<String, List<String>>,
fragmentArgs: Map<String, List<String>>
): UriMatchResult<Session>? {
val finalQueryArgs = buildMap {
putAll(queryArgs)
val jsonBlob = remove("session")?.firstOrNull()
if (jsonBlob != null) { put("session", extractSessionIdFromJson(jsonBlob)) }
}
return super.matchArguments(pathArgs, finalQueryArgs, fragmentArgs)
}val homeMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
listOf(Home, matchResult.key)
}
val sessionMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
listOf(Home, SessionList, matchResult.key)
}
val backStackMatchers: List<BackStackMatcher<NavKey, NavKey>> = listOf(
homeMatcher, sessionMatcher
)
val matchResults = backStackMatchers.mapNotNull { matcher.match(request) }
val bestMatch: BackStackMatchResult<NavKey, NavKey>? = matchResults.maxOrNull()
val backStack = bestMatch?.backStack ?: listOf(Home)@Serializable object ImagesKey: NavKey
// custom filter
val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))val extras = requestExtras {
put(SectionExtrasKey, "A")
}