Nền tảng ứng dụng

Xây dựng đường liên kết sâu an toàn về kiểu bằng Jetpack Navigation3

Loa

  • Clara Fok

    Kỹ sư phần mềm

  • Ian Lake

    Trưởng phòng kỹ thuật

Giới thiệu

Giới thiệu về bài thuyết trình

Việc quản lý đường liên kết sâu trên các ứng dụng Android theo mô-đun có thể dễ dàng dẫn đến các tuyến đường bị hỏng và những bất ngờ trong thời gian chạy.Phiên này sẽ giới thiệu cấu trúc liên kết sâu an toàn về kiểu mới của Jetpack Navigation3 cho Compose để giúp bạn xây dựng một hệ thống định tuyến mạnh mẽ và có thể mở rộng. Chúng ta sẽ tìm hiểu cách triển khai các API so khớp đường liên kết sâu có sẵn, viết trình chuyển đổi tuần tự tuỳ chỉnh cho các đối số an toàn về kiểu phức tạp và xây dựng ngăn xếp lui phân cấp động giúp người dùng dễ dàng điều hướng.

Tóm tắt

  • API liên kết sâu Navigation 3 biến các yêu cầu liên kết sâu đến thành khoá điều hướng để xác định ngăn xếp lui ban đầu. Lớp DeepLinkRequest tương thích với KMP đóng gói dữ liệu đầu vào, bao gồm cả hàm khởi tạo Ý định Android.
  • UriDeepLinkMatcher sử dụng tính năng Chuyển đổi tuần tự Kotlin để so khớp các mẫu URI và chuyển đổi tuần tự các đối số thành các thuộc tính NavKey an toàn về kiểu. Các kiểu dữ liệu gốc hoạt động tự động, không khớp nếu không thể phân tích cú pháp các giá trị.
  • Nhà phát triển có thể mở rộng DeepLinkSerializer hoặc ghi đè matchArguments để xử lý các chuỗi đường liên kết sâu phức tạp. Việc phân lớp con UriDeepLinkMatcher cho phép bạn xử lý trước phần giữ chỗ URI thô trước khi Kotlin Serialization chạy.
  • Việc gọi withBackStack trên trình so khớp sẽ tạo ngăn xếp lui tổng hợp một cách lười biếng để có kết quả khớp nhất. Điều này đảm bảo hành vi quay lại hệ thống phù hợp mà không cần tính toán các ngăn xếp lui không cần thiết cho mọi kết quả khớp hợp lệ khác.

Tài nguyên

Gemini Notebook

Chủ đề

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

Bản chép lời

Giới thiệu và các công thức Navigation 3

  • Xin chào, tôi là Ian Lake, giám đốc kỹ thuật của nhóm Kiến trúc Jetpack.

  • Còn tôi là Clara Fok, trưởng nhóm kỹ thuật của Nav 3.

  • Hôm nay, chúng tôi sẽ giới thiệu một tính năng mà tôi rất tâm đắc, đó là đường liên kết sâu.

  • Thực tế, tôi thích các tính năng này đến mức đã triển khai cho cả Nav2 và Nav3 trong bản phát hành 1.2 của Nav3.

  • Tôi nghĩ bạn thực sự đã làm việc này ba lần, vì chúng ta cũng đã triển khai trong các công thức Nav 3.

  • Đúng vậy.

  • Chúng tôi đã thực hiện nhiều lần lặp lại trên API để đảm bảo API này phù hợp với phương pháp ưu tiên Compose của Nav3.

  • Và phần lớn thành công đó bắt đầu từ việc chúng tôi chú trọng vào công thức nấu ăn.

  • Đúng vậy, một trong những mục tiêu chính của Navigation 3 là giúp bạn dễ dàng sử dụng.

  • Khi bạn muốn xây dựng một tính năng đặc biệt tham vọng, bạn không cần phải gửi yêu cầu về tính năng cho chúng tôi để thực hiện tính năng đó.

  • Vì vậy, khi chúng tôi nhận được câu hỏi: "Được rồi, tôi phải làm thế nào?"

  • Hoặc "Làm thế nào để giải quyết vấn đề này?"

  • Thay vì xây dựng một API tuỳ chỉnh ngay từ đầu, chúng tôi xây dựng một công thức, một hướng dẫn giải thích cách bạn có thể thêm tính năng đó vào ứng dụng mà không nhất thiết phải thay đổi bất kỳ điều gì trong Navigation 3.

  • Chính phong cách "trực quan sinh động" đó đã thôi thúc chúng tôi tạo kho lưu trữ công thức Nav3 trên GitHub.

  • Tài liệu này hiện có hơn 35 công thức cho các trường hợp sử dụng phổ biến như nhiều ngăn xếp lui và bảng dưới cùng, cung cấp cho bạn tất cả mã cần thiết để triển khai trong ứng dụng.

  • Điều đó có nghĩa là ngay cả đối với bản phát hành 1.0 của Nav3 vào tháng 11 năm 2025, chúng tôi đã có công thức triển khai đường liên kết sâu vì chúng tôi biết rằng điều đó rất quan trọng đối với nhiều người trong số các bạn.

  • Nhưng khi nhận ra rằng một công thức yêu cầu có một loạt phương thức trợ giúp mà bạn cần sao chép và dán vào ứng dụng, thì đó là lúc chúng tôi xem xét một quy trình gọi là tốt nghiệp, trong đó chúng tôi lấy những trình trợ giúp đó và nói: "Được rồi, điều đó thực sự sẽ như thế nào nếu chúng là một phần của chính điều hướng ba?"

  • Điều này đảm bảo rằng những nhà phát triển đã sao chép và dán mã đó sẽ không gặp vấn đề gì.

  • Bạn có thể giữ lại mã đó.

  • Nếu bạn có giải pháp tuỳ chỉnh nào đang hoạt động thì thật tuyệt.

  • Bạn không có yêu cầu di chuyển bắt buộc nào.

  • Nhưng nếu đó là một vấn đề hoàn toàn mới hoặc một yêu cầu mới, bạn sẽ có được một giải pháp dễ dàng, đã được kiểm chứng, cho phép bạn sử dụng trực tiếp giải pháp đó như một phần của hoạt động điều hướng.

  • Nhưng điều tuyệt vời nhất đối với cá nhân tôi là sau khi thêm trình trợ giúp đó, chúng tôi quay lại và cập nhật công thức nấu ăn, đồng thời nhận được cảm giác hưng phấn khi xoá mã.

  • Bằng cách đó, bất kỳ ai đang xem công thức để tìm cách giải quyết vấn đề đều có giải pháp mới nhất.

  • Tất nhiên, nhìn lại thì mọi thứ đều rõ ràng, và quá trình tốt nghiệp đó cho chúng ta cơ hội để thực sự xem xét những trình trợ giúp mà chúng ta đã xây dựng trong các công thức và tự hỏi: "Đó có thực sự là những API tốt nhất cho việc đó không?"

  • Và trong trường hợp này, chúng tôi có cơ hội cải thiện những điều đó khi chuyển vào thư viện.

  • Và điều đó chắc chắn đúng với đường liên kết sâu.

Thiết kế API liên kết sâu và so khớp mẫu URI

  • Cảm ơn Ian.

  • Vì vậy, về mặt lý thuyết, cả công thức và API cuối cùng của chúng tôi đều rất giống nhau, vì trong Nav 3, bạn sở hữu Ngăn xếp lui.

  • Điều đó có nghĩa là trọng tâm của các API liên kết sâu là tìm ra Ngăn xếp lui ban đầu, cụ thể là màn hình mà người dùng nhìn thấy khi họ nhấp vào một đường liên kết sâu để vào ứng dụng của bạn.

  • Nhưng điều gì thực sự xảy ra khi người dùng nhấp vào URI web đó?

  • Giờ đây, cho dù bạn có sử dụng Nav3 hay không, khung Android vẫn yêu cầu bạn khai báo Bộ lọc ý định cho mỗi đường liên kết sâu mà ứng dụng của bạn hỗ trợ.

  • Giờ đây, điều thú vị là những gì xảy ra khi đường liên kết sâu đó kích hoạt Hoạt động của bạn bắt đầu.

  • Ở đây, dữ liệu đầu vào là Ý định mà ứng dụng của bạn bắt đầu, Hoạt động đã bắt đầu.

  • Tuy nhiên, đường liên kết sâu có thể có nhiều dạng.

  • Ví dụ: bạn có thể muốn tạo thủ công cho một chương trình kiểm thử hoặc có thể muốn tận dụng tính năng hỗ trợ Kotlin Multiplatform.

  • Vì vậy, chúng tôi đã đóng gói dữ liệu đầu vào của đường liên kết sâu vào một lớp DeepLinkRequest tương thích với KMP.

  • Còn trên Android, chúng ta có một hàm khởi tạo DeepLinkRequest nhận một Ý định Android.

  • Tiếp theo, mục tiêu của chúng ta là tìm hiểu xem khi có một yêu cầu, làm thế nào để biết NavKey nào cần được đưa vào ngăn xếp lui?

  • Đây là lúc cần đến lớp DeepLinkMatcher.

  • Hàm này nhận một yêu cầu và xuất ra một MatchResult, chứa NavKey liên kết với kết quả trùng khớp.

  • Cụ thể đối với URI web, chúng ta có một UriDeepLinkMatcher đóng gói tất cả logic cần thiết để chuyển đổi một yêu cầu nhất định thành MatchResult.

    val matcher = UriDeepLinkMatcher(
      ...,
      ...
    )
  • Giờ đây, tham số đầu tiên mà UriDeepLinkMatcher nhận được là một mẫu URI.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri(...),
      ...
    )
  • Giả sử bạn đang xây dựng một ứng dụng ADS và muốn thêm một đường liên kết sâu vào màn hình chính.

  • Ở đây, mẫu là một URI chính xác và chúng ta có thể bỏ qua lược đồ HTTP để tự động so khớp cả HTTP và HTTPS.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/home"),
      ...
    )
  • Nhưng nếu muốn liên kết sâu đến một phiên quảng cáo cụ thể, thì giờ đây, đường liên kết sâu đó sẽ chứa thông tin mà chúng ta cần trích xuất, cụ thể là mã phiên.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      …
    )
  • Vì vậy, trong trường hợp này, mẫu của chúng ta sử dụng dấu ngoặc nhọn để đóng vai trò là phần giữ chỗ cho thông tin mà chúng ta cần trích xuất từ đường liên kết sâu.

  • Và hoá ra đã có một thư viện tiện dụng để chuyển đổi bất kỳ loại dữ liệu đầu vào nào thành các thực thể lớp, đó là Kotlin Serialization.

  • Vì vậy, UriDeepLinkMatcher của chúng ta tận dụng bộ giải mã Kotlin để giải tuần tự hoá một URI đầu vào và xuất ra một thực thể NavKey.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Bây giờ, bộ giải mã đó cần biết lớp nào cần xử lý, vì vậy, bạn cũng cần truyền một trình chuyển đổi tuần tự cho NavKey vào trình so khớp.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Để giúp bộ giải mã so khớp chính xác phần giữ chỗ với các thuộc tính của khoá, tên của phần giữ chỗ phải khớp với tên thuộc tính.

  • Vì vậy, trong ví dụ này, tên thuộc tính là mã nhận dạng khớp với tham số mã nhận dạng của phiên.

  • Giờ đây, khi đã có UriDeepLinkMatcher, chúng ta cần tạo một DeepLinkRequest bằng đường liên kết sâu sắp tới.

    // 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)
  • Sau đó, quá trình so khớp được thực hiện bằng cách gọi hàm so khớp, truyền vào yêu cầu của chúng ta.

  • Thao tác này sẽ cung cấp cho bạn một MatchResult rỗng chứa NavKey, trong ví dụ của chúng ta sẽ là khoá phiên hoặc nếu kết quả là rỗng, chúng ta có thể chỉ cần đặt mặc định cho màn hình chính.

  • Giờ đây, nhiều đường liên kết sâu phải được viết dưới dạng nhiều trình so khớp: một trình so khớp cho mỗi đường liên kết sâu.

  • Ví dụ: ở đây chúng ta có hai mẫu URI cho khoá phiên: một mẫu cho bản tin cũ và một mẫu cho URI web thông thường.

  • Chúng ta cũng có một đường liên kết sâu đến màn hình chính của ADS, vì vậy, chúng ta sẽ thêm một trình so khớp thứ ba cho đường liên kết đó.

  • Và tất cả trình so khớp sẽ được thêm vào cùng một danh sách mà chúng ta lặp lại.

  • Vì vậy, giờ đây, kết quả khớp của chúng ta sẽ trở thành một vòng lặp trên tất cả các trình so khớp trong danh sách, trong đó chúng ta gọi kết quả khớp trên mỗi trình so khớp để tìm tất cả các trình so khớp khớp với yêu cầu.

  • Để làm việc đó, chúng ta có thể sử dụng mapNotNull để lọc ra các kết quả không khớp và chỉ nhận lại danh sách các kết quả khớp hợp lệ.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
  • Trong hầu hết các trường hợp, có thể bạn sẽ chỉ có một kết quả khớp không rỗng trong danh sách, nhưng cũng có trường hợp có thể có nhiều kết quả khớp hợp lệ.

  • Ví dụ: giả sử chúng ta giới thiệu một màn hình bài phát biểu chính đặc biệt sử dụng lại cùng một mẫu URI phiên, nhưng giờ đây, thay vì phần giữ chỗ mã nhận dạng, màn hình này có một đường dẫn bài phát biểu chính tĩnh.

  • Vì vậy, trong trường hợp này, cùng một yêu cầu có thể khớp với cả hai trình so khớp: khoá phiên có mã nhận dạng bằng keynote và keynote NavKey.

  • Vì MatchResult triển khai Comparable, nên chúng ta có thể tìm thấy kết quả khớp nhất bằng cách so sánh tất cả kết quả khớp hợp lệ trong danh sách bằng hàm max, hoặc thực sự là maxOrNull để xử lý trường hợp không có đường liên kết sâu nào khớp với yêu cầu.

    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
    )
  • Giờ đây, danh sách tĩnh của trình so khớp có thể phù hợp với một toạ đàm nhanh, nhưng cũng giống như cách màn hình của bạn có thể trải rộng trên nhiều mô-đun, chúng tôi mong đợi các mô-đun đó đóng góp vào danh sách trình so khớp chung thông qua hệ thống DI của bạn.

    // 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>>
  • Khi sử dụng phương pháp này, các công thức Hilt, Metro và Koin trong kho lưu trữ công thức của chúng tôi đã được cập nhật để bao gồm các đường liên kết sâu.

Khử chuyển đổi tuần tự an toàn về kiểu và trình chuyển đổi tuần tự tuỳ chỉnh

  • Clara đã đề cập rằng UriDeepLinkMatcher sử dụng tính năng Chuyển đổi tuần tự Kotlin để trích xuất các trường từ yêu cầu liên kết sâu và đưa vào lớp an toàn về kiểu, chẳng hạn như lớp phiên mà chúng tôi đã trình bày.

  • Như được viết ở đây, đây là một phép biến đổi khá đơn giản.

  • URI là một chuỗi.

  • Trường này là một chuỗi.

  • Chà, thật dễ dàng.

  • Nhưng chúng tôi biết rằng chúng tôi cần phải vượt ra ngoài các thuộc tính chuỗi.

  • Đây là nơi Kotlin Serialization đã cung cấp cho chúng ta một nền tảng vững chắc để làm việc.

  • Tất cả các loại dữ liệu gốc, như Boolean, số nguyên, số thực, thậm chí cả Enum đều hoạt động ngay lập tức, tự động không khớp và trả về giá trị rỗng nếu không thể phân tích cú pháp giá trị liên kết sâu dưới dạng loại cụ thể đó.

  • Ví dụ: nếu tham số mã nhận dạng của phiên thực sự là một số nguyên thay vì một chuỗi, thì việc truyền vào một chuỗi không thể phân tích cú pháp thành số nguyên sẽ không khớp.

    // pattern = "http://ads.android.com/session/{id}"
    @Serializable data class Session(val id: Int) : NavKey
  • Tương tự, nếu muốn có các đối số không bắt buộc, chẳng hạn như dữ liệu số liệu phân tích được đính kèm vào URL cho web, thì đó có thể là các thuộc tính trên khoá có giá trị mặc định.

    // pattern = "http://ads.android.com/session/{id}#referrer={referrer}
    @Serializable data class Session(
      val id: Int,
      val referrer: String? = null,
    ) : NavKey
  • Bạn sẽ thấy ở đây rằng chúng tôi đã cố gắng tách biệt cấu trúc của URL cho web (có thể do một nhóm khác sở hữu) khỏi các lớp an toàn về kiểu hoặc các lớp đại diện cho kết quả khớp liên kết sâu trong ứng dụng của bạn.

  • Vì tôi biết một số bạn đang xử lý một số đường liên kết sâu cực kỳ tệ.

    // "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
  • Bạn biết đấy, có thể bạn có một hình ảnh được mã hoá base64 đính kèm vào đường liên kết sâu.

  • Bạn biết đấy, có thể là một blob JSON khổng lồ mà họ quyết định, "Ồ, hãy đưa blob đó vào mọi URL của ứng dụng".

  • Giờ đây, bạn có thể tiếp tục ngay cả khi không thể thay đổi URL cho web được cung cấp, nhưng bạn vẫn cần trích xuất một số thông tin từ URL đó.

  • Vì vậy, kỹ thuật đầu tiên là mở rộng lớp DeepLinkSerializer để kiểm soát chính xác cách đối số đó được chuyển đổi tuần tự hoặc chuyển đổi từ một Chuỗi thành thuộc tính an toàn về kiểu.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
  • DeepLinkSerializer là một trình trợ giúp bao bọc xung quanh Kotlin Serialization, giúp đơn giản hoá việc triển khai lớp trình chuyển đổi tuần tự của chúng, vì trong trường hợp này, dữ liệu đầu vào của trình chuyển đổi tuần tự luôn là một Chuỗi từ URL cho web của bạn.

    public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
  • Bây giờ, để triển khai DeepLinkSerializer, bạn cần viết 3 phương thức.

  • Trước tiên, bạn chọn tên bộ sách.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      ...
    }
  • Không cần phải có không gian tên, nhưng phải đủ riêng biệt để Kotlin Serialization biết thời điểm sử dụng trình chuyển đổi tuần tự của bạn.

  • Tiếp theo, bạn cần triển khai phương thức deserialize. Phương thức này được truyền chuỗi đó từ URL cho web và chịu trách nhiệm tạo đối tượng an toàn về kiểu.

    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)
      }
    
       ...
    
    }
  • Cuối cùng, bạn sẽ triển khai serialize, đây là phương pháp ngược lại và được sử dụng nếu cùng một lớp khoá được dùng trong các trường hợp như 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)
      }
    }
  • Bây giờ, bạn sẽ áp dụng trình chuyển đổi tuần tự tuỳ chỉnh cho thuộc tính có lớp đặc biệt đó và web UriDeepLinkMatcher sẽ xử lý phần còn lại.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
    
    @Serializable data class Session(
      val id: String,
      @Serializable(BitmapSerializer::class)
      val placeholderImage: Bitmap? = null
    ) : NavKey

Xử lý trước đối số URL bằng trình so khớp tuỳ chỉnh

  • Nhưng nếu bạn chỉ muốn một chút dữ liệu từ chuỗi cơ sở 64 đó hoặc JSON được đính kèm vào URL cho web thì sao?

  • Trong trường hợp đó, bạn nên xử lý trước một số URL cho web trước khi chuyển đến Kotlin Serialization.

  • Để xử lý những trường hợp đó, bạn có thể mở rộng UriDeepLinkMatcher và ghi đè phương thức matchArguments của lớp này.

  • Giờ đây, có thể đối với blob JSON, tất cả những gì chúng ta thực sự muốn chỉ là mã phiên đó, như thể chúng ta thực sự không quan tâm đến bất kỳ phần nào còn lại.

  • Vì vậy, chúng ta tạo lớp con và ghi đè 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>? {
        ...
    }
  • Dữ liệu đầu vào là các chuỗi thô, tất cả phần giữ chỗ được thêm từ đường dẫn URI.

  • Và điều đó cho phép chúng ta xử lý trước và chỉnh sửa các giá trị đó trước khi gọi vào 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)
    }
  • Giờ đây, chúng ta có thể có các giá trị đơn giản hoá tại đây và không cần phải can thiệp vào Kotlin Serialization.

Tạo ngăn xếp lui tổng hợp cho đường liên kết sâu

  • Cảm ơn Ian.

  • Hy vọng bạn sẽ không phải thường xuyên sử dụng các kỹ thuật đó, nhưng chúng tôi đã cân nhắc rất nhiều về cách bạn có thể kiểm soát hành vi so khớp.

  • Được rồi.

  • Giờ đây, chúng ta đã biết cách lấy NavKey bằng trình so khớp đường liên kết sâu, nhưng ngăn xếp lui chỉ có khoá điều hướng có thể không mang lại trải nghiệm tốt nhất cho người dùng.

  • Vì vậy, để đảm bảo hành vi quay lại phù hợp, bạn nên sử dụng ngăn xếp lui tổng hợp.

  • Phần tổng hợp xuất phát từ việc ngăn xếp lui chứa các khoá mà người dùng chưa từng di chuyển đến theo cách thủ công, nhưng đó là đường dẫn mà họ sẽ đi nếu họ di chuyển đến đó theo cách thủ công.

  • Ví dụ: khi người dùng nhấp vào thông báo về một phiên sắp bắt đầu, bạn sẽ đưa người dùng đến thẳng phiên đó. Tuy nhiên, khi người dùng kích hoạt nút quay lại của hệ thống, có thể bạn sẽ muốn họ quay lại HomeScreen (Màn hình chính) trước khi thoát ra trình chạy.

  • HomeScreen đó trở thành một phần của Ngăn xếp lui tổng hợp.

  • Giờ đây, đối với API liên kết sâu, chúng tôi muốn cạnh tranh, chúng tôi muốn cân bằng hai yêu cầu cạnh tranh.

  • Một mặt, chúng tôi biết rằng các đường liên kết sâu hoặc trình so khớp khác nhau sẽ cần logic khác nhau để tạo ngăn xếp lui tổng hợp.

  • Ví dụ: việc thêm HomeScreen trước mỗi đường liên kết sâu có thể hoạt động cho đến khi bạn muốn liên kết sâu đến một màn hình phụ bên trong màn hình cài đặt.

  • Bây giờ, bạn cần thêm một lớp vào ngăn xếp lui tổng hợp cho đường liên kết sâu cụ thể đó.

  • Đương nhiên, điều này sẽ liên kết trình tạo ngăn xếp lui với từng trình so khớp liên kết sâu.

  • Nhưng mặt khác, chúng tôi cũng biết rằng mỗi mili giây trước khi màn hình chính xác hiển thị đều khiến bạn tốn tiền thật.

  • Vì vậy, chúng tôi không muốn tính toán ngăn xếp lui tổng hợp cho mọi kết quả trận đấu.

  • Chúng tôi chỉ muốn tìm kết quả phù hợp nhất.

  • Vậy nếu muốn có logic tuỳ chỉnh cho mỗi trình so khớp nhưng không muốn tạo ngăn xếp lui tổng hợp cho mỗi kết quả khớp, chúng ta phải làm gì?

  • Vì vậy, sau một vài lần thử nghiệm không thành công, chúng tôi đã tìm ra một phương pháp cân bằng cả hai yêu cầu.

  • Bất kể bạn có loại trình so khớp nào, bạn đều có thể tăng cường trình so khớp cụ thể đó bằng cách gọi withBackStack để tạo ngăn xếp lui một cách lười biếng bằng MatchResult đã cho.

    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)
  • Vì vậy, nếu có nhiều trình so khớp, mỗi trình so khớp sẽ có lệnh gọi withBackStack riêng để tạo ngăn xếp lui thích hợp cho NavKey cụ thể đó.

  • Bạn sẽ nhận được mã sạch nhất nếu mọi trình so khớp mà bạn gọi đều sử dụng withBackStack, từ đó cung cấp cho bạn danh sách các trình so khớp ngăn xếp lui.

  • Và khi tìm thấy kết quả phù hợp nhất, kết quả khớp mà bạn nhận được là BackStackMatchResult.

  • Đây là một lớp con của MatchResult, bổ sung một thuộc tính mới có tên là backStack. Đây là toàn bộ ngăn xếp lui do hàm lambda withBackStack tạo ra.

  • Phương pháp phân lớp này cho phép chúng tôi tách biệt logic bên trong trình so khớp khỏi bất kỳ logic tuỳ chỉnh nào mà bạn muốn sử dụng bên trong lambda withBackStack.

Tóm tắt, tài nguyên và phần hỏi đáp nhanh

  • Đây là một buổi toạ đàm nhanh nên rất tiếc là chúng ta sắp hết thời gian, nhưng có lẽ chúng ta có thể xem qua một vài tính năng khác trong Navigation 3 mà bạn sẽ tìm thấy trong các công thức của Navigation 3.

  • Được rồi.

  • Vì vậy, bạn có thể thêm bộ lọc vào bất kỳ trình so khớp nào để rút ngắn logic so khớp và nhanh chóng loại trừ trình so khớp dựa trên hành động, loại MIME hoặc bộ lọc tuỳ chỉnh của riêng bạn.

  • Và điều này xảy ra trước khi thực hiện bất kỳ quá trình phân tích cú pháp URI hoặc chuyển đổi tuần tự tuỳ chỉnh nào, tức là Chuyển đổi tuần tự Kotlin.

  • Chúng tôi cũng cung cấp StaticKeyDeepLinkMatcher, đặc biệt cho những trường hợp bạn không cần phân tích cú pháp để lấy NavKey, nhưng vẫn muốn áp dụng bộ lọc.

    @Serializable object ImagesKey: NavKey
    
    // custom filter
    val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
    
    val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
  • Điều này rất phù hợp để xử lý các đường liên kết sâu chỉ có hành động hoặc loại MIME, chẳng hạn như ứng dụng chỉnh sửa ảnh có thể xử lý việc mở hình ảnh JPEG.

  • Nói về hành động và loại MIME, bạn có thể đính kèm bất kỳ RequestExtras tuỳ chỉnh nào vào DeepLinkRequest của mình. Bộ lọc tuỳ chỉnh và lớp con DeepLinkRequest có thể sử dụng các loại MIME này như một phần logic của chúng.

  • Nhờ đó, bạn có thể kết hợp các khung thử nghiệm A/B hoặc tín hiệu bên ngoài.

  • Chúng tôi cũng có toàn bộ API an toàn về kiểu cho các thành phần bổ sung.

    val extras = requestExtras {
        put(SectionExtrasKey, "A")
    }
  • Ồ, và hầu hết logic này đều nằm trong mã KMP chung của Nav3, vì vậy, tất cả logic trình so khớp và ngăn xếp lui đều có thể được chia sẻ trên các nền tảng.

  • Vì vậy, nếu bạn muốn tìm hiểu kỹ hơn về cách Navigation 3 hỗ trợ đường liên kết sâu, hãy xem các công thức Nav3 của chúng tôi.

  • Có, và chúng tôi cũng đã tích hợp API đường liên kết sâu vào các công thức chèn phần phụ thuộc, cũng như ứng dụng chính của chúng tôi, NowInAndroid.

  • Cảm ơn bạn đã tham gia buổi toạ đàm nhanh của chúng tôi. Chúc bạn điều hướng thành công.

  • Cảm ơn bạn.

Mã

Đoạn mã

Đoạn mã Kotlin ở phút 04:41 trong video "Thiết kế API đường liên kết sâu và so khớp mẫu URI"
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
Đoạn mã Kotlin ở phút 04:53 trong video "Thiết kế API đường liên kết sâu và so khớp mẫu URI"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
Đoạn mã Kotlin ở phút 05:04 trong video "Thiết kế API đường liên kết sâu và so khớp mẫu URI"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
Đoạn mã Kotlin ở phút 05:12 trong video "Thiết kế API đường liên kết sâu và so khớp mẫu URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  …
)
Đoạn mã Kotlin ở phút 05:39 trong video "Thiết kế API đường liên kết sâu và so khớp mẫu URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
Đoạn mã Kotlin ở phút 05:48 trong video "Thiết kế API đường liên kết sâu và so khớp mẫu 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>()
)
Đoạn mã Kotlin ở phút 06:12 trong video "Thiết kế API đường liên kết sâu và so khớp mẫu 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)
Đoạn mã Kotlin ở phút 07:11 trong video "Deep link API design and URI pattern matching" (Thiết kế API đường liên kết sâu và so khớp mẫu URI)
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}
Đoạn mã Kotlin ở phút 07:48 trong video "Thiết kế API đường liên kết sâu và so khớp mẫu URI"
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
)
Đoạn mã Kotlin ở phút 08:04 trong video "Thiết kế API liên kết sâu và so khớp mẫu 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>>
Đoạn mã Kotlin ở phút 09:15 trong video "Type-safe deserialization and custom serializers" (Chuyển đổi tuần tự an toàn về kiểu và trình chuyển đổi tuần tự tuỳ chỉnh)
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
Đoạn mã Kotlin ở 09:27 trong phần "Chuyển đổi tuần tự an toàn về kiểu và trình chuyển đổi tuần tự tuỳ chỉnh"
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
Đoạn mã Kotlin tại 09:53 trong phần "Chuyển đổi tuần tự an toàn về kiểu và trình chuyển đổi tuần tự tuỳ chỉnh"
// "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
Đoạn mã Kotlin ở phút 10:19 trong video "Type-safe deserialization and custom serializers" (Chuyển đổi tuần tự an toàn về kiểu và trình chuyển đổi tuần tự tuỳ chỉnh)
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}
Đoạn mã Kotlin ở phút 10:32 trong phần "Chuyển đổi tuần tự an toàn về kiểu và trình chuyển đổi tuần tự tuỳ chỉnh"
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
Đoạn mã Kotlin ở phút 10:51 trong video "Type-safe deserialization and custom serializers" (Chuyển đổi tuần tự an toàn về kiểu và trình chuyển đổi tuần tự tuỳ chỉnh)
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
Đoạn mã Kotlin ở phút 11:01 trong video "Type-safe deserialization and custom serializers" (Chuyển đổi tuần tự an toàn về kiểu và trình chuyển đổi tuần tự tuỳ chỉnh)
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)
  }

   ...

}
Đoạn mã Kotlin ở phút 11:13 trong video "Type-safe deserialization and custom serializers" (Chuyển đổi tuần tự an toàn về kiểu và trình chuyển đổi tuần tự tuỳ chỉnh)
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)
  }
}
Đoạn mã Kotlin ở phút 11:24 trong video "Type-safe deserialization and custom serializers" (Chuyển đổi tuần tự an toàn về kiểu và trình chuyển đổi tuần tự tuỳ chỉnh)
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}

@Serializable data class Session(
  val id: String,
  @Serializable(BitmapSerializer::class)
  val placeholderImage: Bitmap? = null
) : NavKey
Đoạn mã Kotlin ở phút 12:09 trong phần "Xử lý trước đối số URL bằng trình so khớp tuỳ chỉnh"
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>? {
    ...
}
Đoạn mã Kotlin ở 12:21 trong phần "Xử lý trước đối số URL bằng trình so khớp tuỳ chỉnh"
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)
}
Đoạn mã Kotlin ở phút 14:31 trong video "Tạo ngăn xếp lui tổng hợp cho đường liên kết sâu"
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)
Đoạn mã Kotlin ở phút 15:52 trong video "Lightning round, summary, and resources" (Tóm tắt, tài nguyên và phần hỏi đáp nhanh)
@Serializable object ImagesKey: NavKey

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

val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
Đoạn mã Kotlin ở phút 16:24 trong phần "Tóm tắt, tài nguyên và câu hỏi nhanh"
val extras = requestExtras {
    put(SectionExtrasKey, "A")
}