Dasar-dasar aplikasi

Membangun deep link dengan keamanan jenis menggunakan Jetpack Navigation3

Speaker

  • Clara Fok

    Software Engineer

  • Ian Lake

    Manajer Teknis

Tentang

Tentang konferensi

Mengelola deep link di seluruh aplikasi Android modular dapat dengan mudah menyebabkan rute rusak dan kejutan runtime.Sesi ini membahas arsitektur deep linking keamanan jenis baru Jetpack Navigation3 untuk Compose guna membantu Anda membangun sistem pemilihan rute yang tangguh dan skalabel. Kita akan mempelajari cara menerapkan API pencocokan deep link siap pakai, menulis serializer kustom untuk argumen type-safe yang kompleks, dan membuat data sebelumnya hierarkis dinamis yang menjaga navigasi pengguna tetap dapat diprediksi.

Ringkasan

  • API deep link Navigation 3 mengubah permintaan deep link yang masuk menjadi kunci navigasi untuk menentukan data sebelumnya awal Anda. Class DeepLinkRequest yang kompatibel dengan KMP mengenkapsulasi input, termasuk konstruktor Intent Android.
  • UriDeepLinkMatcher menggunakan Serialisasi Kotlin untuk mencocokkan pola URI dan mendeserialisasi argumen menjadi properti NavKey yang aman dari jenis. Jenis primitif berfungsi secara otomatis, sehingga pencocokan akan gagal jika nilai tidak dapat diuraikan.
  • Developer dapat memperluas DeepLinkSerializer atau mengganti matchArguments untuk menangani string deep link yang kompleks. Membuat subclass UriDeepLinkMatcher memungkinkan Anda melakukan pra-pemrosesan placeholder URI mentah sebelum Serialisasi Kotlin berjalan.
  • Memanggil withBackStack pada matcher akan membuat data sintetis sebelumnya secara lambat untuk kecocokan terbaik. Hal ini memastikan perilaku kembali sistem yang tepat tanpa menghitung data sebelumnya yang tidak perlu untuk setiap kecocokan valid lainnya.

Resource

Gemini Notebook

Topik

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

Transkrip

Pengantar dan resep Navigation 3

  • Halo, saya Ian Lake, Engineering Manager untuk tim Jetpack Architecture.

  • Dan saya Clara Fok, tech lead untuk Nav 3.

  • Hari ini, kami akan membahas fitur yang sangat penting bagi saya, Nav-- deep link.

  • Bahkan, saya sangat menyukainya sehingga saya melakukan implementasi untuk Nav2 dan Nav3 sebagai bagian dari rilis 1.2 Nav3.

  • Sepertinya Anda sudah melakukannya tiga kali, karena kami juga melakukan implementasi dalam resep Nav 3.

  • Ya, benar.

  • Kami melakukan banyak iterasi pada API untuk memastikan API tersebut cocok dengan pendekatan Nav3 yang mengutamakan Compose.

  • Dan banyak dari hal itu dimulai dengan penekanan kami pada resep.

  • Ya, salah satu sasaran utama kami untuk Navigation 3 adalah agar tidak mengganggu Anda.

  • Jika Anda ingin membuat fitur yang sangat ambisius, Anda tidak perlu menunggu permintaan fitur dari kami untuk mewujudkannya.

  • Jadi, saat kita mendapatkan pertanyaan, "Baiklah, bagaimana cara melakukannya?"

  • Atau, "Bagaimana cara mengatasi masalah ini?"

  • Alih-alih membuat API kustom sejak awal, kami membuat resep, yaitu panduan yang menjelaskan cara menambahkan fitur tersebut ke aplikasi Anda tanpa perlu mengubah Navigation 3 sama sekali.

  • Gaya menunjukkan, bukan memberi tahu, itulah yang mendorong kami membuat repositori resep Nav3 di GitHub.

  • Sekarang, panduan ini memiliki lebih dari 35 resep untuk kasus penggunaan umum seperti beberapa data sebelumnya dan panel bawah yang memberi Anda semua kode yang diperlukan untuk mengimplementasikannya di aplikasi Anda.

  • Artinya, bahkan untuk rilis 1.0 Nav3, pada November 2025, kami sudah memiliki resep untuk mengimplementasikan deep link karena kami tahu bahwa hal itu penting bagi banyak dari Anda.

  • Namun, saat kami menyadari bahwa sebuah resep memerlukan banyak metode bantuan yang perlu Anda salin dan tempel ke aplikasi, saat itulah kami mempertimbangkan proses yang disebut kelulusan, di mana kami mengambil bantuan tersebut dan berkata, "Baiklah, seperti apa sebenarnya jika bantuan tersebut menjadi bagian dari navigasi tiga itu sendiri?"

  • Hal ini memastikan bahwa developer yang telah menyalin dan menempelkan kode tersebut tidak akan mengalami masalah.

  • Anda dapat menyimpan kode tersebut.

  • Jika solusi kustom yang Anda miliki berfungsi, itu bagus.

  • Anda tidak memiliki migrasi paksa.

  • Namun, jika ini adalah masalah baru bagi Anda atau persyaratan baru, Anda akan mendapatkan solusi yang mudah dan telah diperiksa yang memungkinkan Anda menggunakannya secara langsung sebagai bagian dari navigasi.

  • Namun, bagian terbaiknya, bagi saya pribadi, adalah setelah menambahkan helper tersebut, kita kembali dan mengupdate resep, lalu mendapatkan kepuasan tersendiri karena menghapus kode.

  • Dengan begitu, siapa pun yang melihat resep untuk pendekatan yang direkomendasikan untuk masalah tersebut akan memiliki solusi terbaru.

  • Tentu saja, kita bisa melihat ke belakang dengan jelas, dan proses kelulusan tersebut memberi kita kesempatan untuk benar-benar melihat helper yang kita buat dalam resep dan berkata, "Apakah itu benar-benar API terbaik untuk hal tersebut?"

  • Dalam hal ini, kita berkesempatan untuk meningkatkan kualitasnya saat memindahkannya ke koleksi.

  • Dan itulah yang terjadi pada deep link.

Desain API deep link dan pencocokan pola URI

  • Terima kasih, Ian.

  • Jadi, secara konseptual, resep dan API akhir kami sangat mirip, karena di Nav 3, Anda memiliki Data Sebelumnya.

  • Artinya, fokus API deep link kami adalah mencari tahu apa yang seharusnya menjadi Data Sebelumnya awal, khususnya layar apa yang dilihat pengguna saat mereka mengklik deep link untuk masuk ke aplikasi Anda.

  • Namun, apa yang sebenarnya terjadi saat pengguna mengklik URI web tersebut?

  • Sekarang, baik Anda menggunakan Nav3 atau tidak, framework Android mengharuskan Anda mendeklarasikan Filter Intent untuk setiap deep link yang didukung aplikasi Anda.

  • Sekarang, hal yang menarik adalah apa yang terjadi setelah deep link tersebut memicu Aktivitas Anda untuk dimulai.

  • Di sini, inputnya adalah Intent yang digunakan untuk memulai aplikasi Anda, yaitu Aktivitas yang dimulai.

  • Namun, deep link dapat memiliki banyak bentuk.

  • Misalnya, Anda mungkin ingin membuatnya secara manual untuk pengujian, atau Anda mungkin ingin memanfaatkan dukungan Kotlin Multiplatform.

  • Jadi, kami telah mengenkapsulasi input deep link ke dalam class DeepLinkRequest yang kompatibel dengan KMP.

  • Dan di Android, kita memiliki konstruktor DeepLinkRequest yang menggunakan Intent Android.

  • Selanjutnya, tujuan kita adalah mencari tahu, berdasarkan permintaan, bagaimana kita mengetahui NavKey apa yang perlu ditempatkan di data sebelumnya?

  • Di sinilah class DeepLinkMatcher berperan.

  • Fungsi ini menerima permintaan dan menghasilkan MatchResult, yang berisi NavKey yang terkait dengan kecocokan.

  • Khusus untuk URI web, kami memiliki UriDeepLinkMatcher yang mengenkapsulasi semua logika yang diperlukan untuk mengubah permintaan tertentu menjadi MatchResult.

    val matcher = UriDeepLinkMatcher(
      ...,
      ...
    )
  • Sekarang, parameter pertama yang digunakan UriDeepLinkMatcher adalah pola URI.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri(...),
      ...
    )
  • Jadi, misalnya Anda sedang membuat aplikasi ADS, dan ingin menambahkan deep link ke layar utama.

  • Di sini, polanya adalah URI yang sama persis, dan kita dapat mengabaikan skema HTTP untuk otomatis mencocokkan HTTP dan HTTPS.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/home"),
      ...
    )
  • Namun, jika kita ingin membuat deep link ke sesi ADS tertentu, deep link tersebut kini berisi informasi yang perlu kita ekstrak, yaitu ID sesi.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      …
    )
  • Jadi, dalam hal ini, pola kita menggunakan tanda kurung kurawal untuk bertindak sebagai placeholder untuk informasi yang perlu kita ekstrak dari deep link.

  • Ternyata sudah ada library praktis untuk mengonversi semua jenis input menjadi instance class, yaitu Kotlin Serialization.

  • Jadi, UriDeepLinkMatcher kami memanfaatkan decoder Kotlin untuk melakukan deserialisasi URI input dan menghasilkan instance NavKey.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Sekarang, decoder tersebut perlu mengetahui class yang akan digunakan, jadi Anda juga perlu meneruskan serializer untuk NavKey ke 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>()
    )
  • Untuk membantu decoder mencocokkan placeholder dengan properti kunci dengan benar, nama placeholder harus cocok dengan nama properti.

  • Jadi, dalam contoh ini, nama propertinya adalah ID yang cocok dengan parameter ID sesi.

  • Oke, setelah memiliki UriDeepLinkMatcher, kita perlu membuat DeepLinkRequest dengan deep link yang masuk.

    // 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)
  • Kemudian, pencocokan dilakukan dengan memanggil fungsi pencocokan, yang meneruskan permintaan kita.

  • Tindakan ini akan memberi Anda MatchResult nullable yang berisi NavKey, yang dalam contoh kita adalah kunci sesi, atau jika hasilnya null, kita dapat menetapkan layar utama sebagai default.

  • Sekarang, beberapa deep link harus ditulis sebagai beberapa matcher: satu matcher untuk setiap deep link.

  • Misalnya, di sini kita memiliki dua pola URI untuk kunci sesi: satu untuk pola newsletter lama dan satu untuk URI web reguler.

  • Kita juga memiliki deep link ke layar utama ADS, jadi kita menambahkan matcher ketiga untuk itu.

  • Dan semua matcher akan ditambahkan ke daftar yang sama yang kita iterasi.

  • Jadi, sekarang pencocokan kita menjadi loop di semua pencocok dalam daftar, tempat kita memanggil pencocokan di setiap pencocok untuk menemukan semua pencocok yang cocok dengan permintaan.

  • Untuk melakukannya, kita dapat menggunakan mapNotNull untuk memfilter data yang tidak cocok dan mendapatkan kembali daftar yang hanya berisi kecocokan yang valid.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
  • Dalam sebagian besar kasus, Anda mungkin hanya akan memiliki satu kecocokan non-null dalam daftar, tetapi ada kasus saat bisa ada lebih dari satu kecocokan yang valid.

  • Misalnya, kita memperkenalkan layar keynote khusus yang menggunakan kembali pola URI sesi yang sama, tetapi sekarang memiliki jalur keynote statis, bukan placeholder ID.

  • Jadi, dalam kasus ini, permintaan yang sama dapat cocok dengan kedua pencocok: kunci sesi dengan ID yang sama dengan keynote dan keynote NavKey.

  • Jadi, karena MatchResult mengimplementasikan Comparable, kita dapat menemukan kecocokan terbaik dengan membandingkan semua kecocokan yang valid dalam daftar menggunakan fungsi max, atau maxOrNull untuk menangani kasus saat tidak ada deep link yang cocok dengan permintaan.

    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
    )
  • Daftar pencocok statis mungkin cocok untuk lightning talk, tetapi seperti halnya layar Anda yang dapat tersebar di beberapa modul, kami mengharapkan modul yang sama tersebut berkontribusi pada daftar pencocok global melalui sistem DI Anda.

    // 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>>
  • Dengan pendekatan ini, resep Hilt, Metro, dan Koin di repositori resep kami telah diperbarui untuk menyertakan deep link.

Deserialisasi yang aman dari jenis dan serializer kustom

  • Clara menyebutkan bahwa UriDeepLinkMatcher menggunakan Serialisasi Kotlin untuk mengekstrak kolom dari permintaan deep link dan mengubahnya menjadi class yang aman dari jenis, seperti class sesi yang kami tunjukkan.

  • Seperti yang tertulis di sini, ini adalah transformasi yang cukup sederhana.

  • URI adalah string.

  • Kolom ini berupa string.

  • Wah, mudah sekali.

  • Namun, kami tahu bahwa kami perlu melampaui properti string saja.

  • Di sinilah Serialisasi Kotlin memberi kita fondasi yang kuat untuk digunakan.

  • Semua jenis primitif, seperti Boolean, bilangan bulat, float, bahkan Enum, langsung berfungsi, otomatis menggagalkan pencocokan dan menampilkan null jika nilai deep link tidak dapat diuraikan sebagai jenis tertentu.

  • Misalnya, jika parameter ID sesi kita sebenarnya adalah bilangan bulat, bukan string, meneruskan string yang tidak dapat diuraikan dalam bilangan bulat akan menggagalkan pencocokan.

    // pattern = "http://ads.android.com/session/{id}"
    @Serializable data class Session(val id: Int) : NavKey
  • Demikian pula, jika Anda ingin memiliki argumen opsional, misalnya untuk data analisis yang dilampirkan ke URL web, argumen tersebut dapat berupa properti pada kunci Anda yang memiliki nilai default.

    // pattern = "http://ads.android.com/session/{id}#referrer={referrer}
    @Serializable data class Session(
      val id: Int,
      val referrer: String? = null,
    ) : NavKey
  • Anda akan melihat di sini bahwa kami telah berusaha keras untuk memisahkan struktur URL web Anda, yang mungkin dimiliki oleh tim yang berbeda, dari class atau class yang aman dari jenis yang mewakili kecocokan deep link di aplikasi Anda.

  • Karena kami tahu beberapa dari Anda menangani deep link yang sangat buruk.

    // "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
  • Misalnya, Anda mungkin memiliki gambar berenkode base64 yang dilampirkan ke deep link.

  • Misalnya, blob JSON raksasa yang mereka putuskan, "Oh, mari kita masukkan saja ke setiap URL aplikasi Anda".

  • Sekarang, ada cara untuk mengatasinya, bahkan saat Anda tidak dapat mengubah URL web yang diberikan, tetapi Anda masih perlu mengekstrak beberapa informasi dari URL tersebut.

  • Jadi, teknik pertama adalah memperluas class DeepLinkSerializer untuk mengontrol secara tepat cara argumen tersebut dideserialisasi atau diubah dari String menjadi properti yang aman untuk jenis.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
  • DeepLinkSerializer adalah wrapper bantuan di sekitar Kotlin Serialization yang menyederhanakan penerapan class serializer-nya, karena dalam hal ini, input deserializer selalu berupa String dari URL web Anda.

    public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
  • Sekarang, untuk mengimplementasikan DeepLinkSerializer, Anda menulis tiga metode.

  • Pertama, pilih nama serial.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      ...
    }
  • Tidak perlu diberi namespace, tetapi harus cukup unik agar Kotlin Serialization tahu kapan harus menggunakan serializer Anda.

  • Selanjutnya, Anda perlu mengimplementasikan deserialize, yang meneruskan String tersebut dari URL web Anda dan bertanggung jawab untuk membuat objek yang aman jenisnya.

    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)
      }
    
       ...
    
    }
  • Terakhir, Anda akan menerapkan serialisasi, yang merupakan pendekatan yang berlawanan, dan ini digunakan jika class kunci yang sama digunakan dalam kasus seperti 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)
      }
    }
  • Sekarang Anda akan menerapkan serializer kustom ke properti yang memiliki class yang sedikit istimewa, dan UriDeepLinkMatcher web akan menangani sisanya.

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

Melakukan pra-pemrosesan argumen URL dengan pencocok kustom

  • Namun, bagaimana jika Anda hanya menginginkan sedikit data dari String base 64 tersebut, atau JSON yang dilampirkan ke URL web Anda?

  • Dalam hal ini, sebaiknya lakukan pra-pemrosesan beberapa URL web Anda sebelum masuk ke Kotlin Serialization.

  • Untuk menangani kasus tersebut, Anda dapat memperluas UriDeepLinkMatcher dan mengganti metode matchArguments-nya.

  • Sekarang, mungkin untuk blob JSON kita, yang benar-benar kita inginkan hanyalah ID sesi tersebut, seperti kita sebenarnya tidak peduli dengan hal lainnya.

  • Jadi, kita membuat subclass dan mengganti 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>? {
        ...
    }
  • Inputnya adalah string mentah tersebut, semua placeholder yang ditambahkan dari jalur URI Anda.

  • Hal ini memungkinkan kita melakukan pra-pemrosesan dan mengedit nilai tersebut sebelum memanggil 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)
    }
  • Sekarang, kita dapat memiliki nilai yang disederhanakan di sini, dan kita tidak perlu repot-repot menggunakan Kotlin Serialization sama sekali.

Membuat data sebelumnya yang bersifat sintetis untuk deep link

  • Terima kasih, Ian.

  • Semoga Anda tidak perlu sering menggunakan teknik tersebut, tetapi kami telah mempertimbangkan dengan cermat cara Anda dapat mengontrol perilaku pencocokan.

  • Oke.

  • Sekarang kita tahu cara mendapatkan NavKey dengan pencocok deep link, tetapi data sebelumnya yang hanya berisi kunci navigasi mungkin tidak memberikan pengalaman pengguna terbaik.

  • Jadi, untuk memastikan perilaku kembali yang tepat, Anda memerlukan data sintetis sebelumnya.

  • Bagian sintetis berasal dari fakta bahwa data sebelumnya berisi kunci yang tidak pernah dibuka secara manual oleh pengguna, tetapi merupakan jalur yang akan mereka ambil jika mereka membuka secara manual ke sana.

  • Jadi, misalnya, mengklik notifikasi untuk sesi yang akan dimulai, Anda akan mengarahkan pengguna langsung ke sesi tersebut, tetapi saat mereka memicu kembali sistem, Anda mungkin ingin mereka kembali ke Layar Utama sebelum keluar ke peluncur.

  • HomeScreen tersebut menjadi bagian dari Data Sebelumnya Sintetis.

  • Untuk API deep link, kami ingin menyeimbangkan dua persyaratan yang saling bertentangan.

  • Di satu sisi, kita tahu bahwa deep link yang berbeda, atau matcher yang berbeda, akan menginginkan logika yang berbeda untuk membangun data sebelumnya sintetisnya.

  • Jadi, misalnya, menambahkan HomeScreen sebelum setiap deep link mungkin akan berfungsi hingga Anda ingin melakukan deep link ke sub-layar di dalam layar setelan.

  • Sekarang, Anda memerlukan lapisan tambahan di data sebelumnya sintetis untuk deep link tertentu tersebut.

  • Tentu saja, hal ini mengaitkan builder data sebelumnya ke setiap pencocok deep link.

  • Namun, di sisi lain, kami juga tahu bahwa setiap milidetik yang diperlukan untuk menampilkan layar yang benar akan merugikan Anda.

  • Jadi, kita tidak ingin menghitung data sintetis sebelumnya untuk setiap hasil pencocokan.

  • Kami hanya menginginkan kecocokan terbaik.

  • Jadi, jika kita menginginkan logika kustom untuk setiap matcher, tetapi tidak ingin membuat data sebelumnya sintetis secara eager untuk setiap kecocokan, apa yang harus kita lakukan?

  • Jadi, setelah beberapa kali gagal, kami menemukan pendekatan yang menyeimbangkan kedua persyaratan tersebut.

  • Apa pun jenis matcher yang Anda miliki, Anda dapat menambah matcher tertentu tersebut dengan memanggil withBackStack untuk membuat data sebelumnya secara lambat dengan MatchResult yang diberikan.

    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)
  • Jadi, jika kita memiliki beberapa matcher, masing-masing akan memiliki panggilan withBackStack sendiri untuk membuat data sebelumnya yang sesuai untuk NavKey tertentu.

  • Anda akan mendapatkan kode yang paling rapi jika setiap matcher yang Anda panggil menggunakan withBackStack, yang akan memberi Anda daftar matcher data sebelumnya.

  • Dan saat Anda menemukan kecocokan terbaik, hasil kecocokan yang Anda dapatkan adalah BackStackMatchResult.

  • Ini adalah subclass dari MatchResult yang menambahkan properti baru bernama backStack, yang merupakan seluruh data sebelumnya yang dihasilkan oleh lambda withBackStack Anda.

  • Pendekatan berlapis ini memungkinkan kami memisahkan logika di dalam matcher dari logika kustom apa pun yang ingin Anda gunakan di dalam lambda withBackStack.

Sesi kilat, ringkasan, dan referensi

  • Sekarang, ini adalah lightning talk, jadi sayangnya, waktu kita hampir habis, tetapi mungkin kita dapat membahas beberapa fitur lain yang kita miliki di navigasi tiga yang akan Anda temukan di resep nav tiga kami.

  • Oke.

  • Jadi, Anda dapat menambahkan filter ke pencocok mana pun untuk mempersingkat logika pencocokan dan dengan cepat mengecualikan pencocok berdasarkan tindakan, jenis MIME, atau filter kustom Anda sendiri.

  • Hal ini terjadi sebelum melalui penguraian URI atau serialisasi kustom, Serialisasi Kotlin.

  • Kami juga menawarkan StaticKeyDeepLinkMatcher, khususnya untuk kasus saat Anda tidak perlu melakukan penguraian apa pun untuk mendapatkan NavKey, tetapi masih ingin menerapkan filter.

    @Serializable object ImagesKey: NavKey
    
    // custom filter
    val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
    
    val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
  • Hal ini cocok untuk menangani deep link khusus tindakan atau jenis MIME, seperti aplikasi pengeditan foto yang dapat menangani pembukaan gambar JPEG.

  • Terkait tindakan dan jenis MIME, Anda dapat melampirkan RequestExtras kustom ke DeepLinkRequest, yang dapat digunakan oleh filter kustom dan subclass DeepLinkRequest sebagai bagian dari logikanya.

  • Hal ini memungkinkan Anda menggabungkan framework pengujian AB atau sinyal eksternal.

  • Kami juga memiliki API yang sepenuhnya aman dari jenis untuk tambahan.

    val extras = requestExtras {
        put(SectionExtrasKey, "A")
    }
  • Oh, dan hampir semua logika ini ada dalam kode KMP umum Nav3, sehingga semua logika matcher dan data sebelumnya dapat dibagikan di seluruh platform.

  • Jadi, jika Anda ingin mempelajari lebih lanjut cara Navigation 3 mendukung deep link, lihat resep Nav3 kami.

  • Ya, dan kami juga telah mengintegrasikan API deep link ke dalam resep DI, serta aplikasi unggulan kami, NowInAndroid.

  • Terima kasih telah menghadiri lightning talk kami, dan selamat menjelajah.

  • Terima kasih.

Kode

Cuplikan kode

Cuplikan Kotlin pada menit 04:41 dalam "Desain API deep link dan pencocokan pola URI"
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
Cuplikan Kotlin pada 04:53 di "Desain API deep link dan pencocokan pola URI"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
Cuplikan Kotlin pada menit 05.04 di "Desain API deep link dan pencocokan pola URI"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
Cuplikan Kotlin pada 05:12 di "Desain API deep link dan pencocokan pola URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  …
)
Cuplikan Kotlin pada 05:39 di "Desain API deep link dan pencocokan pola URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
Cuplikan Kotlin pada 05:48 di "Desain API deep link dan pencocokan pola 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>()
)
Cuplikan Kotlin pada 06:12 di "Desain API deep link dan pencocokan pola 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)
Cuplikan Kotlin pada menit 07:11 di "Desain API deep link dan pencocokan pola URI"
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}
Cuplikan Kotlin pada menit 07:48 di "Desain API deep link dan pencocokan pola 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
)
Cuplikan Kotlin pada menit 08:04 di "Desain API deep link dan pencocokan pola 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>>
Cuplikan Kotlin pada 09:15 di "Deserialisasi yang aman dari jenis dan serializer kustom"
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
Cuplikan Kotlin pada 09:27 di "Deserialisasi yang aman dari jenis dan serializer kustom"
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
Cuplikan Kotlin pada 09:53 di "Deserialisasi yang aman dari jenis dan serializer kustom"
// "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
Cuplikan Kotlin pada menit 10:19 di "Deserialisasi yang aman dari jenis dan serializer kustom"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}
Cuplikan Kotlin pada 10:32 di "Deserialisasi yang aman dari jenis dan serializer kustom"
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
Cuplikan Kotlin pada menit 10:51 di "Type-safe deserialization and custom serializers"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
Cuplikan Kotlin pada menit 11:01 di "Deserialisasi yang aman dari jenis dan serializer kustom"
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)
  }

   ...

}
Cuplikan Kotlin pada menit 11:13 di "Deserialisasi yang aman dari jenis dan serializer kustom"
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)
  }
}
Cuplikan Kotlin pada menit 11:24 di "Deserialisasi yang aman dari jenis dan serializer kustom"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}

@Serializable data class Session(
  val id: String,
  @Serializable(BitmapSerializer::class)
  val placeholderImage: Bitmap? = null
) : NavKey
Cuplikan Kotlin pada 12:09 di "Melakukan pra-pemrosesan argumen URL dengan pencocok kustom"
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>? {
    ...
}
Cuplikan Kotlin pada menit 12:21 di "Melakukan pra-pemrosesan argumen URL dengan pencocok kustom"
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)
}
Cuplikan Kotlin pada 14:31 di "Membuat data sebelumnya sintetis untuk 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)
Cuplikan Kotlin pada 15:52 di "Sesi singkat, ringkasan, dan referensi"
@Serializable object ImagesKey: NavKey

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

val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
Cuplikan Kotlin pada 16:24 di "Sesi singkat, ringkasan, dan referensi"
val extras = requestExtras {
    put(SectionExtrasKey, "A")
}