วิศวกรซอฟต์แวร์
การจัดการ Deep Link ในแอป Android แบบโมดูลาร์อาจทำให้เส้นทางเสียและเกิดปัญหาขณะรันไทม์ได้ง่ายๆ เซสชันนี้จะอธิบายสถาปัตยกรรม Deep Link แบบ Type-Safe ใหม่ของ Jetpack Navigation3 สำหรับ Compose เพื่อช่วยคุณสร้างระบบการกำหนดเส้นทางที่มีประสิทธิภาพและปรับขนาดได้ เราจะสำรวจวิธีใช้ API การจับคู่ Deep Link ที่พร้อมใช้งานทันที เขียน Serializer ที่กำหนดเองสำหรับอาร์กิวเมนต์ที่ปลอดภัยต่อประเภทที่ซับซ้อน และสร้าง Back Stack แบบลำดับชั้นแบบไดนามิกที่ช่วยให้การนำทางของผู้ใช้เป็นไปตามที่คาดการณ์ได้
DeepLinkRequest ที่เข้ากันได้กับ KMP จะรวมอินพุตไว้ ซึ่งรวมถึงคอนสตรักเตอร์ Android IntentUriDeepLinkMatcher ใช้การแปลง Kotlin เป็นอนุกรมเพื่อจับคู่รูปแบบ URI และดีซีเรียลไลซ์อาร์กิวเมนต์เป็นพร็อพเพอร์ตี้ NavKey ที่ปลอดภัยในการกำหนดประเภท ประเภทพื้นฐานจะทำงานโดยอัตโนมัติ และจะจับคู่ไม่สำเร็จหากแยกวิเคราะห์ค่าไม่ได้DeepLinkSerializer หรือลบล้าง matchArguments เพื่อจัดการสตริง Deep Link ที่ซับซ้อนได้ การสร้างคลาสย่อย UriDeepLinkMatcher ช่วยให้คุณประมวลผลตัวยึดตำแหน่ง URI ดิบไว้ล่วงหน้าก่อนที่การจัดรูปแบบ Kotlin จะทำงานwithBackStack ใน Matcher จะสร้าง Back Stack สังเคราะห์แบบ Lazy เพื่อให้ได้ผลลัพธ์ที่ดีที่สุด วิธีนี้ช่วยให้มั่นใจได้ว่าระบบจะทำงานได้อย่างถูกต้องโดยไม่ต้องคำนวณ Back Stack ที่ไม่จำเป็นสำหรับการจับคู่ที่ถูกต้องอื่นๆ ทั้งหมดข้อความถอดเสียง
สวัสดี ฉันชื่อ Ian Lake เป็นผู้จัดการฝ่ายวิศวกรรมของทีม Jetpack Architecture
และฉันชื่อ Clara Fok เป็นหัวหน้าฝ่ายเทคนิคของ Nav 3
และวันนี้เราจะมาพูดถึงฟีเจอร์ที่ฉันชอบมาก นั่นก็คือ Nav-- Deep Link
อันที่จริง ฉันชอบ Nav2 และ Nav3 มากจนได้ติดตั้งใช้งานทั้ง 2 อย่างเป็นส่วนหนึ่งของ Nav3 เวอร์ชัน 1.2
ฉันคิดว่าคุณทำไปแล้ว 3 ครั้ง เพราะเราได้ทำการติดตั้งใช้งานในสูตร Nav 3 ด้วย
ใช่ ถูกต้อง
เราได้ทำการปรับปรุง API หลายครั้งเพื่อให้แน่ใจว่า API จะทำงานร่วมกับแนวทางที่เน้น Compose เป็นหลักของ Nav3 ได้อย่างราบรื่น
และส่วนใหญ่ก็เริ่มต้นจากการที่เราเน้นเรื่องสูตรอาหาร
ใช่ เป้าหมายหลักอย่างหนึ่งของเราสำหรับ Navigation 3 คือการไม่รบกวนคุณ
เมื่อคุณมีฟีเจอร์ที่ทะเยอทะยานเป็นพิเศษที่ต้องการสร้าง คุณไม่จำเป็นต้องส่งคำขอฟีเจอร์จากเราเพื่อทำให้ฟีเจอร์นั้นเป็นจริง
ดังนั้น เมื่อเราได้รับคำถามว่า "ฉันจะทำแบบนี้ได้อย่างไร"
หรือ "ฉันจะแก้ปัญหานี้ได้อย่างไร"
แทนที่จะสร้าง API ที่กำหนดเองตั้งแต่เริ่มต้น เราจะสร้างสูตรอาหาร ซึ่งเป็นคู่มือที่อธิบายวิธีเพิ่มฟีเจอร์นั้นลงในแอปโดยไม่จำเป็นต้องทำการเปลี่ยนแปลงใดๆ ใน Navigation 3 เลย
สไตล์การแสดงให้เห็นแทนที่จะบอกเล่านี้เองที่ทำให้เราสร้างที่เก็บสูตรอาหาร Nav3 บน GitHub
ตอนนี้มีสูตรอาหารมากกว่า 35 รายการสำหรับ Use Case ทั่วไป เช่น Back Stack หลายรายการและ Bottom Sheet ที่ให้โค้ดทั้งหมดที่คุณต้องใช้ในการติดตั้งใช้งานในแอป
ซึ่งหมายความว่าแม้แต่ใน Nav3 เวอร์ชัน 1.0 ที่เปิดตัวเมื่อเดือนพฤศจิกายน 2025 เราก็มีสูตรสำหรับการใช้ Deep Link อยู่แล้ว เพราะเรารู้ว่าสิ่งนี้สำคัญสำหรับหลายๆ คน
แต่เมื่อเราตระหนักว่าสูตรอาหารต้องมีเมธอดตัวช่วยจำนวนมากที่คุณต้องคัดลอกและวางลงในแอป นั่นคือตอนที่เราพิจารณากระบวนการที่เรียกว่าการสำเร็จการศึกษา ซึ่งเราจะนำตัวช่วยเหล่านั้นมาพิจารณาว่า "เอาล่ะ จะเป็นอย่างไรหากตัวช่วยเหล่านั้นเป็นส่วนหนึ่งของ Navigation 3 เอง"
วิธีนี้ช่วยให้มั่นใจว่านักพัฒนาแอปที่คัดลอกและวางโค้ดดังกล่าวไปแล้วจะไม่มีปัญหาใดๆ
คุณเก็บรหัสไว้ได้
หากคุณมีโซลูชันที่กำหนดเองและใช้งานได้ก็ถือว่าดีมาก
คุณไม่มีการย้ายข้อมูลที่บังคับ
แต่หากเป็นปัญหาใหม่หรือข้อกำหนดใหม่สำหรับคุณ คุณจะได้รับโซลูชันที่ผ่านการตรวจสอบแล้วและใช้งานง่าย ซึ่งช่วยให้คุณใช้โซลูชันนั้นเป็นส่วนหนึ่งของการนำทางได้โดยตรง
แต่ส่วนที่ดีที่สุดสำหรับฉันคือ หลังจากเพิ่มตัวช่วยนั้นแล้ว เราจะกลับไปอัปเดตสูตรอาหารและได้รับสารโดพามีนที่ทำให้รู้สึกดีจากการลบโค้ด
ด้วยวิธีนี้ ทุกคนที่ดูสูตรอาหารสำหรับแนวทางที่แนะนำสำหรับปัญหานั้นจะมีวิธีแก้ปัญหาที่อัปเดตล่าสุด
แน่นอนว่าเรามองย้อนกลับไปได้เสมอ และกระบวนการจบการศึกษาดังกล่าวเปิดโอกาสให้เราได้พิจารณาตัวช่วยที่เราสร้างขึ้นในสูตรอาหารอย่างละเอียด และถามตัวเองว่า "นั่นเป็น API ที่ดีที่สุดสำหรับสูตรอาหารนั้นจริงๆ หรือไม่"
และในกรณีนี้ เรามีโอกาสที่จะปรับปรุงสิ่งเหล่านั้นเมื่อย้ายไปยังคลัง
ซึ่ง Deep Link ก็เป็นหนึ่งในนั้น
ขอบคุณ Ian
ดังนั้นในเชิงแนวคิด ทั้งสูตรและ API สุดท้ายของเราจึงคล้ายกันมาก เพราะใน Nav 3 คุณเป็นเจ้าของ Back Stack
ซึ่งหมายความว่าจุดมุ่งหมายของ API ของ Deep Link คือการพิจารณาว่า Back Stack เริ่มต้นควรเป็นอย่างไร โดยเฉพาะหน้าจอที่ผู้ใช้เห็นเมื่อคลิก Deep Link เพื่อเข้าสู่แอป
แต่จะเกิดอะไรขึ้นเมื่อผู้ใช้คลิก URI ของเว็บนั้น
ตอนนี้ ไม่ว่าคุณจะใช้ Nav3 หรือไม่ เฟรมเวิร์ก Android กำหนดให้คุณต้องประกาศตัวกรอง Intent สำหรับ Deep Link แต่ละรายการที่แอปรองรับ
สิ่งที่น่าสนใจคือสิ่งที่จะเกิดขึ้นเมื่อ Deep Link นั้นทริกเกอร์ให้กิจกรรมของคุณเริ่มต้นขึ้น
ในกรณีนี้ อินพุตคือ Intent ที่แอปของคุณเริ่มต้นด้วย ซึ่งก็คือ Activity ที่เริ่มต้น
แต่ Deep Link มีได้หลายรูปแบบ
เช่น คุณอาจต้องการสร้างด้วยตนเองเพื่อทดสอบ หรืออาจต้องการใช้ประโยชน์จากการรองรับ Kotlin Multiplatform
ดังนั้น เราจึงได้รวมอินพุต Deep Link ไว้ในคลาส DeepLinkRequest ที่เข้ากันได้กับ KMP
และใน Android เรามีเครื่องมือสร้าง DeepLinkRequest ที่รับ Android Intent
ถัดไป เป้าหมายของเราคือการพิจารณาว่าเมื่อได้รับคำขอแล้ว เราจะทราบได้อย่างไรว่าต้องใส่ NavKey อะไรลงใน Back Stack
นี่คือจุดที่คลาส DeepLinkMatcher ของเราเข้ามามีบทบาท
โดยจะรับคำขอและแสดงผล MatchResult ซึ่งมี NavKey ที่เชื่อมโยงกับการจับคู่
โดยเฉพาะสำหรับ URI ของเว็บ เรามี UriDeepLinkMatcher ที่รวมตรรกะทั้งหมดที่จำเป็นในการเปลี่ยนคำขอหนึ่งๆ ให้เป็น MatchResult
val matcher = UriDeepLinkMatcher(
...,
...
)ตอนนี้พารามิเตอร์แรกที่ UriDeepLinkMatcher ใช้คือรูปแบบ URI
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri(...),
...
)สมมติว่าคุณกำลังสร้างแอป ADS และต้องการเพิ่ม Deep Link ลงในหน้าจอหลัก
ในกรณีนี้ รูปแบบคือ URI ที่ตรงกันทุกประการ และเราสามารถละเว้นสคีมา HTTP เพื่อจับคู่ทั้ง HTTP และ HTTPS โดยอัตโนมัติได้
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/home"),
...
)แต่หากต้องการทำ Deep Link ไปยังเซสชันโฆษณาหนึ่งๆ ตอนนี้ Deep Link นั้นจะมีข้อมูลที่เราต้องดึงออกมา ซึ่งก็คือรหัสเซสชัน
// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
…
)ดังนั้นในกรณีนี้ รูปแบบของเราจะใช้วงเล็บปีกกาเพื่อทำหน้าที่เป็นตัวยึดตำแหน่งสำหรับข้อมูลที่เราต้องดึงมาจาก Deep Link
และปรากฏว่ามีไลบรารีที่ใช้งานง่ายอยู่แล้วสำหรับแปลงอินพุตทุกประเภทเป็นอินสแตนซ์คลาส ซึ่งก็คือ 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>()
)ตอนนี้ตัวถอดรหัสนั้นจำเป็นต้องทราบคลาสที่จะทำงานด้วย ดังนั้นคุณจึงต้องส่งผ่าน Serializer สำหรับ NavKey ของคุณไปยัง Matcher ด้วย
// 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 ด้วย Deep Link ขาเข้า
// 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 เราก็สามารถใช้หน้าจอหลักเป็นค่าเริ่มต้นได้
ตอนนี้ คุณควรเขียน Deep Link หลายรายการเป็น Matcher หลายรายการ โดยให้มี Matcher 1 รายการสำหรับ Deep Link แต่ละรายการ
ตัวอย่างเช่น เรามีรูปแบบ URI 2 รูปแบบสำหรับเซสชันคีย์ ได้แก่ รูปแบบหนึ่งสำหรับรูปแบบจดหมายข่าวเดิม และอีกรูปแบบหนึ่งสำหรับ URI ของเว็บปกติ
และเรายังมี Deep Link ไปยังหน้าจอหลักของ ADS ด้วย เราจึงเพิ่ม Matcher รายการที่ 3 สำหรับหน้าจอดังกล่าว
และระบบจะเพิ่ม Matcher ทั้งหมดลงในรายการเดียวกันที่เราทำซ้ำ
ดังนั้นตอนนี้การจับคู่ของเราจะกลายเป็นลูปเหนือตัวจับคู่ทั้งหมดในรายการ ซึ่งเราจะเรียกใช้การจับคู่ในแต่ละรายการเพื่อค้นหาตัวจับคู่ทั้งหมดที่ตรงกับคำขอ
ในการดำเนินการดังกล่าว เราสามารถใช้ mapNotNull เพื่อกรองรายการที่ไม่ตรงกันออกและรับรายการเฉพาะรายการที่ตรงกันที่ถูกต้องกลับมา
val matchers = listOf(...)
// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
matcher.match(request)
}ในกรณีส่วนใหญ่ คุณอาจมีรายการที่ตรงกันซึ่งไม่ใช่ค่า Null เพียงรายการเดียวในรายการ แต่ก็มีบางกรณีที่อาจมีรายการที่ตรงกันที่ถูกต้องมากกว่า 1 รายการ
ตัวอย่างเช่น สมมติว่าเราเปิดตัวหน้าจอประเด็นสำคัญพิเศษที่ใช้รูปแบบ URI ของเซสชันเดียวกันซ้ำ แต่ตอนนี้มีเส้นทางประเด็นสำคัญแบบคงที่แทนตัวยึดตำแหน่งรหัส
ดังนั้นในกรณีนี้ คำขอเดียวกันจึงสามารถจับคู่กับทั้ง 2 ตัวจับคู่ได้ ซึ่งก็คือเซสชันคีย์ที่รหัสเท่ากับคีย์โน้ตและคีย์โน้ต NavKey
ดังนั้นเนื่องจาก MatchResult ใช้ Comparable เราจึงสามารถค้นหารายการที่ตรงกันที่สุดได้โดยการเปรียบเทียบรายการที่ตรงกันที่ถูกต้องทั้งหมดในรายการโดยใช้ฟังก์ชัน max หรือ maxOrNull เพื่อจัดการกรณีที่ไม่มี Deep Link ที่ตรงกับคำขอ
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
)ตอนนี้รายการ Matcher แบบคงที่อาจเหมาะสำหรับ Lightning Talk แต่เช่นเดียวกับที่หน้าจอของคุณสามารถกระจายไปตามโมดูลต่างๆ เราคาดหวังว่าโมดูลเดียวกันเหล่านั้นจะช่วยสร้างรายการ Matcher ส่วนกลางผ่านระบบ 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 ในที่เก็บสูตรของเราได้รับการอัปเดตให้รวม Deep Link แล้ว
Clara ได้พูดถึงว่า UriDeepLinkMatcher ของเราใช้ Kotlin Serialization เพื่อดึงฟิลด์ออกจากคำขอ Deep Link และทำให้เป็นคลาสที่ปลอดภัยในการกำหนดประเภท เช่น คลาสเซสชันที่เราแสดง
ตามที่เขียนไว้ที่นี่ การเปลี่ยนรูปแบบนี้ค่อนข้างง่าย
URI เป็นสตริง
ช่องนี้เป็นสตริง
ว้าว ง่ายมากเลย
แต่เราทราบดีว่าต้องทำมากกว่าแค่พร็อพเพอร์ตี้สตริง
ซึ่ง Kotlin Serialization มีรากฐานที่มั่นคงให้เราทำงานด้วยอยู่แล้ว
ประเภทพื้นฐานทั้งหมด เช่น บูลีน, จำนวนเต็ม, ทศนิยม หรือแม้แต่ Enum จะทำงานได้ทันที โดยจะทำให้การจับคู่ไม่สำเร็จโดยอัตโนมัติและแสดงผลเป็น Null หากไม่สามารถแยกวิเคราะห์ค่า Deep Link เป็นประเภทที่เฉพาะเจาะจงนั้นได้
เช่น หากพารามิเตอร์รหัสของเซสชันเป็นจำนวนเต็มแทนที่จะเป็นสตริง การส่งสตริงที่แยกวิเคราะห์เป็นจำนวนเต็มไม่ได้จะทำให้การจับคู่ไม่สำเร็จ
// 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 ของเว็บ ซึ่งอาจเป็นของทีมอื่น ออกจากคลาสที่ปลอดภัยในการกำหนดประเภทหรือคลาสที่แสดงถึงการจับคู่ Deep Link ในแอป
เพราะเราทราบดีว่าบางคนกำลังจัดการกับ Deep Link ที่แย่มากๆ
// "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 แนบมากับ Deep Link
คุณอาจมี Blob ของ JSON ขนาดใหญ่ที่ตัดสินใจว่า "โอ้ เรามาใส่ข้อมูลนั้นลงใน URL ทุกรายการของแอปกันเถอะ"
ตอนนี้มีวิธีแก้ปัญหาแล้ว แม้ว่าคุณจะเปลี่ยน URL ของเว็บที่ได้รับไม่ได้ แต่คุณยังคงต้องดึงข้อมูลบางอย่างออกจาก URL นั้น
ดังนั้น เทคนิคแรกคือการขยายคลาส DeepLinkSerializer เพื่อควบคุมวิธีดีซีเรียลไลซ์หรือเปลี่ยนอาร์กิวเมนต์นั้นจากสตริงเป็นพร็อพเพอร์ตี้ที่ปลอดภัยในการกำหนดประเภทอย่างแม่นยำ
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
}DeepLinkSerializer เป็น Wrapper ตัวช่วยรอบ Kotlin Serialization ที่ช่วยลดความซับซ้อนในการติดตั้งใช้งานคลาส Serializer เนื่องจากในกรณีนี้ อินพุต Deserializer จะเป็นสตริงจาก URL ของเว็บเสมอ
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>ตอนนี้หากต้องการใช้ DeepLinkSerializer คุณต้องเขียน 3 เมธอด
ขั้นแรก ให้เลือกชื่อซีรีส์
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
override val serialName: String
get() = "Bitmap"
...
}ไม่จำเป็นต้องมีเนมสเปซ แต่ต้องไม่ซ้ำกันมากพอเพื่อให้ Kotlin Serialization ทราบว่าควรใช้ Serializer ของคุณเมื่อใด
ถัดไป คุณต้องใช้การดีซีเรียลไลซ์ ซึ่งจะส่งสตริงนั้นจาก 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)
}
...
}สุดท้าย คุณจะใช้การทำให้เป็นอนุกรม ซึ่งเป็นแนวทางที่ตรงกันข้าม และจะใช้ในกรณีที่ใช้คีย์คลาสเดียวกันในกรณีต่างๆ เช่น 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 ที่กำหนดเองกับพร็อพเพอร์ตี้ที่มีคลาสที่พิเศษเล็กน้อย และเว็บ 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>? {
...
}อินพุตคือสตริงดิบเหล่านั้น ซึ่งเป็นตัวยึดตำแหน่งทั้งหมดที่เพิ่มจากเส้นทาง 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 เลย
ขอบคุณ Ian
เราหวังว่าคุณจะไม่ต้องใช้เทคนิคเหล่านั้นบ่อยนัก แต่เราได้พิจารณาอย่างรอบคอบเกี่ยวกับวิธีที่คุณจะควบคุมลักษณะการทำงานของการจับคู่ได้
ตกลง
ตอนนี้เราทราบวิธีรับ NavKey ด้วย Matcher ของ Deep Link แล้ว แต่ Back Stack ที่มีเฉพาะคีย์การนําทางอาจไม่ได้มอบประสบการณ์ที่ดีที่สุดแก่ผู้ใช้
ดังนั้น เพื่อให้แน่ใจว่าลักษณะการทำงานของปุ่มย้อนกลับเป็นไปอย่างถูกต้อง คุณจึงควรใช้ Back Stack สังเคราะห์
ส่วนสังเคราะห์มาจากการที่ Back Stack มีคีย์ที่ผู้ใช้ไม่เคยไปยังด้วยตนเอง แต่เป็นเส้นทางที่ผู้ใช้จะใช้หากไปยังคีย์นั้นด้วยตนเอง
ตัวอย่างเช่น การคลิกการแจ้งเตือนสำหรับเซสชันที่กำลังจะเริ่มจะนำผู้ใช้ไปยังเซสชันนั้นโดยตรง แต่เมื่อผู้ใช้เรียกใช้การย้อนกลับของระบบ คุณอาจต้องการให้ผู้ใช้กลับไปที่หน้าจอหลักก่อนที่จะออกจาก Launcher
หน้าจอหลักนั้นจะกลายเป็นส่วนหนึ่งของ Synthetic Back Stack
สำหรับ API ของ Deep Link เราต้องการแข่งขันและสร้างสมดุลระหว่างข้อกำหนด 2 ข้อที่แข่งขันกัน
ในด้านหนึ่ง เราทราบว่า Deep Link หรือ Matcher ที่แตกต่างกันต้องการตรรกะที่แตกต่างกันในการสร้าง Back Stack สังเคราะห์
ตัวอย่างเช่น การเพิ่ม HomeScreen ก่อน Deep Link ทุกรายการอาจใช้ได้จนกว่าคุณจะต้องการทำ Deep Link ไปยังหน้าจอย่อยภายในหน้าจอการตั้งค่า
ตอนนี้คุณต้องมีเลเยอร์เพิ่มเติมใน Back Stack สังเคราะห์สำหรับ Deep Link นั้นๆ
ซึ่งจะเชื่อมโยง Back Stack Builder กับ Matcher ของ Deep Link แต่ละรายการโดยอัตโนมัติ
แต่ในทางกลับกัน เราก็ทราบดีว่าทุกๆ มิลลิวินาทีที่ใช้ในการแสดงหน้าจอที่ถูกต้องนั้นทำให้คุณเสียเงินจริง
เราจึงไม่ต้องการคำนวณ Back Stack สังเคราะห์สำหรับผลการค้นหาที่ตรงกันทุกรายการ
เราต้องการเพียงการจับคู่ที่ตรงกันมากที่สุด
ดังนั้น หากเราต้องการตรรกะที่กำหนดเองสำหรับ Matcher แต่ละรายการ แต่ไม่ต้องการสร้าง Back Stack สังเคราะห์อย่างกระตือรือร้นสำหรับการจับคู่แต่ละรายการ เราควรทำอย่างไร
ดังนั้น หลังจากที่ลองผิดลองถูกอยู่หลายครั้ง เราก็พบแนวทางที่สร้างสมดุลระหว่างข้อกำหนดทั้ง 2 ข้อได้
ไม่ว่าคุณจะมี Matcher ประเภทใด คุณก็สามารถเพิ่ม Matcher ที่เฉพาะเจาะจงนั้นได้โดยเรียกใช้ withBackStack เพื่อสร้าง Back Stack แบบ Lazy ด้วย 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)ดังนั้น หากเรามี Matcher หลายรายการ แต่ละรายการก็จะมี withBackStack ของตัวเองเพื่อสร้าง Back Stack ที่เหมาะสมสำหรับ NavKey นั้นๆ
คุณจะได้รับโค้ดที่สะอาดที่สุดหาก Matcher ทุกรายการที่คุณเรียกใช้ withBackStack ซึ่งจะแสดงรายการ Matcher ของ Back Stack
และเมื่อพบรายการที่ตรงกันที่สุด ผลลัพธ์ที่ตรงกันที่คุณได้รับกลับมาคือ BackStackMatchResult
นี่คือคลาสย่อยของ MatchResult ที่เพิ่มพร็อพเพอร์ตี้ใหม่ชื่อ backStack ซึ่งเป็น Back Stack ทั้งหมดที่สร้างโดย Lambda withBackStack ของคุณ
แนวทางการแบ่งชั้นนี้ช่วยให้เราแยกตรรกะภายใน Matcher ออกจากตรรกะที่กำหนดเองที่คุณต้องการใช้ภายใน Lambda ของ withBackStack ได้
เนื่องจากนี่เป็น Lightning Talk เราจึงมีเวลาจำกัด แต่เราอาจพูดถึงฟีเจอร์อื่นๆ บางอย่างที่มีใน Navigation 3 ซึ่งคุณจะพบในสูตรอาหารของ Nav 3
ตกลง
ดังนั้น คุณจึงเพิ่มตัวกรองลงในตัวจับคู่เพื่อลัดวงจรตรรกะการจับคู่และยกเว้นตัวจับคู่ได้อย่างรวดเร็วตามการดำเนินการ ประเภท MIME หรือตัวกรองที่กำหนดเองของคุณเอง
และขั้นตอนนี้จะเกิดขึ้นก่อนที่จะผ่านการแยกวิเคราะห์ URI หรือการแปลงเป็นอนุกรมที่กำหนดเอง ซึ่งก็คือ Kotlin Serialization
นอกจากนี้ เรายังมีStaticKeyDeepLinkMatcherสำหรับกรณีที่คุณไม่จำเป็นต้องแยกวิเคราะห์เพื่อรับ NavKey แต่ยังคงต้องการใช้ตัวกรอง
@Serializable object ImagesKey: NavKey
// custom filter
val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))ซึ่งเหมาะอย่างยิ่งสำหรับการจัดการ Deep Link ที่มีการดำเนินการหรือประเภท MIME เท่านั้น เช่น แอปแต่งรูปที่จัดการการเปิดรูปภาพ JPEG ได้
และเมื่อพูดถึงการดำเนินการและประเภท MIME คุณสามารถแนบ RequestExtras ที่กำหนดเองกับ DeepLinkRequest ซึ่งตัวกรองที่กำหนดเองและคลาสย่อย DeepLinkRequest สามารถใช้เป็นส่วนหนึ่งของตรรกะได้
ซึ่งจะช่วยให้คุณผสมผสานเฟรมเวิร์กการทดสอบ AB หรือสัญญาณภายนอกได้
นอกจากนี้ เรายังมี API ที่ปลอดภัยในการกำหนดประเภททั้งหมดสำหรับส่วนเสริมด้วย
val extras = requestExtras {
put(SectionExtrasKey, "A")
}อ้อ และตรรกะเกือบทั้งหมดนี้อยู่ในโค้ด KMP ทั่วไปของ Nav3 ดังนั้นตรรกะ Matcher และ Back Stack ทั้งหมดจึงแชร์ข้ามแพลตฟอร์มได้
ดังนั้น หากต้องการดูวิธีที่ Navigation 3 รองรับ Deep Link อย่างละเอียด โปรดดูสูตร Nav3 ของเรา
ใช่ และเรายังได้ผสานรวม API ของ Deep Link เข้ากับสูตร DI รวมถึงแอปหลักของเราอย่าง NowInAndroid ด้วย
ขอขอบคุณที่เข้าร่วม Lightning Talk ของเรา และขอให้สนุกกับการใช้งาน
ขอขอบคุณ
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")
}