Apps foundation

การสร้าง Deep Link ที่ปลอดภัยในการกำหนดประเภทด้วย Jetpack Navigation 3

ลำโพง

  • Clara Fok

    วิศวกรซอฟต์แวร์

  • Ian Lake

    ผู้จัดการฝ่ายวิศวกรรม

เกี่ยวกับ

เกี่ยวกับการเสวนา

การจัดการ Deep Link ในแอป Android แบบโมดูลาร์อาจทำให้เส้นทางเสียและเกิดปัญหาขณะรันไทม์ได้ง่ายๆ เซสชันนี้จะอธิบายสถาปัตยกรรม Deep Link แบบ Type-Safe ใหม่ของ Jetpack Navigation3 สำหรับ Compose เพื่อช่วยคุณสร้างระบบการกำหนดเส้นทางที่มีประสิทธิภาพและปรับขนาดได้ เราจะสำรวจวิธีใช้ API การจับคู่ Deep Link ที่พร้อมใช้งานทันที เขียน Serializer ที่กำหนดเองสำหรับอาร์กิวเมนต์ที่ปลอดภัยต่อประเภทที่ซับซ้อน และสร้าง Back Stack แบบลำดับชั้นแบบไดนามิกที่ช่วยให้การนำทางของผู้ใช้เป็นไปตามที่คาดการณ์ได้

สรุป

  • Navigation 3 Deep Link API จะเปลี่ยนคำขอ Deep Link ขาเข้าเป็นคีย์การนำทางเพื่อกำหนด Back Stack เริ่มต้น คลาส DeepLinkRequest ที่เข้ากันได้กับ KMP จะรวมอินพุตไว้ ซึ่งรวมถึงคอนสตรักเตอร์ Android Intent
  • UriDeepLinkMatcher ใช้การแปลง Kotlin เป็นอนุกรมเพื่อจับคู่รูปแบบ URI และดีซีเรียลไลซ์อาร์กิวเมนต์เป็นพร็อพเพอร์ตี้ NavKey ที่ปลอดภัยในการกำหนดประเภท ประเภทพื้นฐานจะทำงานโดยอัตโนมัติ และจะจับคู่ไม่สำเร็จหากแยกวิเคราะห์ค่าไม่ได้
  • นักพัฒนาแอปสามารถขยาย DeepLinkSerializer หรือลบล้าง matchArguments เพื่อจัดการสตริง Deep Link ที่ซับซ้อนได้ การสร้างคลาสย่อย UriDeepLinkMatcher ช่วยให้คุณประมวลผลตัวยึดตำแหน่ง URI ดิบไว้ล่วงหน้าก่อนที่การจัดรูปแบบ Kotlin จะทำงาน
  • การเรียก withBackStack ใน Matcher จะสร้าง Back Stack สังเคราะห์แบบ Lazy เพื่อให้ได้ผลลัพธ์ที่ดีที่สุด วิธีนี้ช่วยให้มั่นใจได้ว่าระบบจะทำงานได้อย่างถูกต้องโดยไม่ต้องคำนวณ Back Stack ที่ไม่จำเป็นสำหรับการจับคู่ที่ถูกต้องอื่นๆ ทั้งหมด

แหล่งข้อมูล

Gemini Notebook

หัวข้อ

  • jetpack-navigation-3
  • deep-links
  • kotlin-serialization
  • kotlin-multiplatform
  • android-architecture

ข้อความถอดเสียง

บทนำและการนำทาง 3 สูตรอาหาร

  • สวัสดี ฉันชื่อ 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 ก็เป็นหนึ่งในนั้น

การออกแบบ Deep Link API และการจับคู่รูปแบบ URI

  • ขอบคุณ 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

การประมวลผลอาร์กิวเมนต์ URL ล่วงหน้าด้วย Matcher ที่กำหนดเอง

  • แต่จะทำอย่างไรหากคุณต้องการข้อมูลเพียงเล็กน้อยจากสตริง 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 เลย

การสร้าง Back Stack สังเคราะห์สำหรับ Deep Link

  • ขอบคุณ 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 ของเรา และขอให้สนุกกับการใช้งาน

  • ขอขอบคุณ

รหัส

ข้อมูลโค้ด

ข้อมูลโค้ด Kotlin ที่ 04:41 ใน "การออกแบบ Deep Link API และการจับคู่รูปแบบ URI"
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
ข้อมูลโค้ด Kotlin ที่ 04:53 ใน "การออกแบบ Deep Link API และการจับคู่รูปแบบ URI"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
ข้อมูลโค้ด Kotlin ที่ 05:04 ใน "การออกแบบ API ของ Deep Link และการจับคู่รูปแบบ URI"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
ข้อมูลโค้ด Kotlin ที่ 05:12 ใน "การออกแบบ API ของ Deep Link และการจับคู่รูปแบบ URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  …
)
ข้อมูลโค้ด Kotlin ที่ 05:39 ใน "การออกแบบ Deep Link API และการจับคู่รูปแบบ URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
ข้อมูลโค้ด Kotlin ที่ 05:48 ใน "การออกแบบ API ของ Deep Link และการจับคู่รูปแบบ URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
ข้อมูลโค้ด Kotlin ที่ 06:12 ใน "การออกแบบ Deep Link API และการจับคู่รูปแบบ URI"
// 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)
ข้อมูลโค้ด Kotlin ที่นาที 07:11 ใน "การออกแบบ Deep Link API และการจับคู่รูปแบบ URI"
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}
ข้อมูลโค้ด Kotlin ที่นาที 07:48 ใน "Deep link API design and URI pattern matching"
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
)
ข้อมูลโค้ด Kotlin ที่ 08:04 ใน "การออกแบบ Deep Link API และการจับคู่รูปแบบ URI"
// 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>>
ข้อมูลโค้ด Kotlin ที่นาที 09:15 ใน "Type-safe deserialization and custom serializers"
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
ข้อมูลโค้ด Kotlin ที่นาที 09:27 ใน "Type-safe deserialization and custom serializers"
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
ข้อมูลโค้ด Kotlin ที่นาที 09:53 ใน "Type-safe deserialization and custom serializers"
// "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
ข้อมูลโค้ด Kotlin ที่นาที 10:19 ใน "Type-safe deserialization and custom serializers"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}
ข้อมูลโค้ด Kotlin ที่นาที 10:32 ใน "Type-safe deserialization and custom serializers"
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
ข้อมูลโค้ด Kotlin ที่นาที 10:51 ใน "Type-safe deserialization and custom serializers"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
ข้อมูลโค้ด Kotlin ที่ 11:01 ใน "Type-safe deserialization and custom serializers"
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)
  }

   ...

}
ข้อมูลโค้ด Kotlin ที่ 11:13 ใน "Type-safe deserialization and custom serializers"
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)
  }
}
ข้อมูลโค้ด Kotlin ที่นาที 11:24 ใน "Type-safe deserialization and custom serializers"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}

@Serializable data class Session(
  val id: String,
  @Serializable(BitmapSerializer::class)
  val placeholderImage: Bitmap? = null
) : NavKey
ข้อมูลโค้ด Kotlin ที่ 12:09 ใน "การประมวลผลอาร์กิวเมนต์ URL ล่วงหน้าด้วย Matcher ที่กำหนดเอง"
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>? {
    ...
}
ข้อมูลโค้ด Kotlin ที่ 12:21 ใน "การประมวลผลอาร์กิวเมนต์ URL ล่วงหน้าด้วย Matcher ที่กำหนดเอง"
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 ที่ 14:31 ใน "การสร้าง Back Stack สังเคราะห์สำหรับ Deep Link"
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)
ข้อมูลโค้ด Kotlin ที่ 15:52 ใน "Lightning round, summary, and resources"
@Serializable object ImagesKey: NavKey

// custom filter
val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")

val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
ข้อมูลโค้ด Kotlin ที่ 16:24 ใน "Lightning round, summary, and resources"
val extras = requestExtras {
    put(SectionExtrasKey, "A")
}