Uygulama temeli

Jetpack Navigation3 ile tür güvenli derin bağlantılar oluşturma

Hoparlörler

  • Clara Fok

    Yazılım Mühendisi

  • Ian Lake

    Mühendislik Müdürü

Hakkında

Konuşma hakkında

Modüler Android uygulamalarında derin bağlantıları yönetmek, kolayca bozuk rotalara ve çalışma zamanında sürprizlere yol açabilir.Bu oturumda, sağlam ve ölçeklenebilir bir rota sistemi oluşturmanıza yardımcı olmak için Jetpack Navigation3'ün Compose için yeni, tür güvenli derin bağlantı mimarisi ele alınacak. Kullanıma hazır derin bağlantı eşleştirme API'lerini nasıl uygulayacağınızı, karmaşık tip güvenliği bağımsız değişkenleri için özel serileştiriciler yazmayı ve kullanıcı gezinmesini öngörülebilir kılan dinamik hiyerarşik eski yığınlar oluşturmayı öğreneceksiniz.

Özet

  • Navigation 3 derin bağlantı API'leri, gelen derin bağlantı isteklerini gezinme tuşlarına dönüştürerek ilk eski yığınınızı belirler. KMP uyumlu DeepLinkRequest sınıfı, Android Intent oluşturucu dahil olmak üzere girişi kapsar.
  • UriDeepLinkMatcher, URI kalıplarını eşleştirmek ve bağımsız değişkenleri tür açısından güvenli NavKey özelliklerine dönüştürmek için Kotlin serileştirmesini kullanır. İlkel türler otomatik olarak çalışır ve değerler ayrıştırılamazsa eşleşmeler başarısız olur.
  • Geliştiriciler, karmaşık derin bağlantı dizelerini işlemek için DeepLinkSerializer sınıfını genişletebilir veya matchArguments yöntemini geçersiz kılabilir. UriDeepLinkMatcher alt sınıfını oluşturarak Kotlin serileştirme işlemi çalıştırılmadan önce ham URI yer tutucularını önceden işleyebilirsiniz.
  • Eşleştiricilerde withBackStack çağrısı, en iyi eşleşme için sentetik eski yığınları geç oluşturur. Bu sayede, diğer tüm geçerli eşleşmeler için gereksiz eski yığınlar hesaplanmadan sistemin geri düğmesi davranışı doğru şekilde sağlanır.

Kaynaklar

Gemini Notebook

Konular

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

Transkript

Giriş ve Gezinme 3 tarif

  • Merhaba, ben Ian Lake. Jetpack Architecture ekibinin mühendislik müdürüyüm.

  • Ben de Clara Fok, Nav 3'ün teknik lideriyim.

  • Bugün, benim için çok önemli bir özellik olan derin bağlantılardan bahsedeceğiz.

  • Hatta bu özellikleri o kadar çok beğendim ki Nav3'ün 1.2 sürümünün bir parçası olarak hem Nav2 hem de Nav3 için uygulama yaptım.

  • Sanırım bunu üç kez yaptınız çünkü Nav 3 tariflerinde de bir uygulama yaptık.

  • Evet, bu doğru.

  • Nav3'ün Compose öncelikli yaklaşımına uygun olduğundan emin olmak için API'de birçok iterasyon yaptık.

  • Bu başarının büyük bir kısmı, tariflere verdiğimiz önemle başladı.

  • Evet, Navigasyon 3'teki temel hedeflerimizden biri, size engel olmamaktı.

  • Özellikle iddialı bir özellik oluşturmak istediğinizde, bunu mümkün kılmak için bizden özellik isteğinde bulunmanız gerekmez.

  • "Peki, bunu nasıl yapacağım?" sorusuyla karşılaştığımızda

  • Ya da "Bu soruna nasıl yaklaşabilirim?" diye düşünebilirsiniz.

  • En başından özel bir API oluşturmak yerine, Navigation 3'te herhangi bir değişiklik yapmanıza gerek kalmadan bu özelliği uygulamanıza nasıl ekleyebileceğinizi açıklayan bir kılavuz hazırladık.

  • Söylemek yerine göstermeyi tercih ettiğimiz için GitHub'da Nav3 tarifleri deposunu oluşturduk.

  • Artık birden fazla eski yığın ve alt sayfa gibi yaygın kullanım alanları için 35'ten fazla tarif içeriyor. Bu tarifler, uygulamanızda kullanmanız gereken tüm kodları sağlıyor.

  • Bu sayede, Kasım 2025'te Nav3'ün 1.0 sürümü yayınlandığında bile derin bağlantıları uygulayacak bir yöntemimiz vardı. Çünkü bunun çoğunuz için önemli olduğunu biliyorduk.

  • Ancak bir tarifin, uygulamanıza kopyalayıp yapıştırmanız gereken bir dizi yardımcı yöntem gerektirdiğini fark ettiğimizde, "mezuniyet" adı verilen bir süreci değerlendiririz. Bu süreçte, bu yardımcıları alır ve "Peki, bunlar Navigation 3'ün bir parçası olsaydı nasıl görünürdü?" diye sorarız.

  • Bu sayede, kodu kopyalayıp yapıştıran geliştiriciler sorun yaşamaz.

  • Bu kodu saklayabilirsiniz.

  • Özel çözümleriniz varsa ve bunlar işe yarıyorsa harika.

  • Zorunlu taşıma işleminiz yok.

  • Ancak bu sizin için yepyeni bir sorun veya yeni bir gereksinimse, bunu doğrudan navigasyonun bir parçası olarak kullanmanıza olanak tanıyan kolay ve onaylanmış bir çözüm elde edersiniz.

  • Ancak benim için en iyi kısım, bu yardımcıyı ekledikten sonra geri dönüp tariflerimizi güncellememiz ve kod silmenin verdiği o tatlı dopamin hissini yaşamamız.

  • Böylece, bu sorun için önerilen yaklaşım türüne yönelik tariflere bakan herkes en güncel çözüme sahip olur.

  • Tabii ki sonradan akıl vermek kolaydır. Bu mezuniyet süreci, tariflerde oluşturduğumuz yardımcı araçlara gerçekten bakıp "Bunlar gerçekten en iyi API'ler mi?" diye sorma fırsatı veriyor.

  • Bu durumda, kitaplığa taşırken bu öğeleri iyileştirme fırsatımız oluyor.

  • Derin bağlantılar için de durum kesinlikle böyleydi.

Derin bağlantı API tasarımı ve URI kalıbı eşleştirme

  • Teşekkürler Ian.

  • Bu nedenle, kavramsal olarak hem tarifler hem de son API'lerimiz birbirine çok benzer. Çünkü Nav 3'te eski yığın size aittir.

  • Yani derin bağlantı API'lerimizin odak noktası, ilk eski yığının ne olması gerektiğini, özellikle de kullanıcılarınız uygulamanıza girmek için bir derin bağlantıyı tıkladığında hangi ekranı göreceğini belirlemektir.

  • Peki kullanıcı bu web URI'sini tıkladığında gerçekte ne olur?

  • Artık Nav3 kullansanız da kullanmasanız da Android çerçevesi, uygulamanızın desteklediği her derin bağlantı için intent filtreleri beyan etmenizi gerektirir.

  • Şimdi, asıl ilginç olan kısma, yani derin bağlantı etkinliğinizi başlattıktan sonra ne olduğuna bakalım.

  • Burada giriş, uygulamanızın başlatıldığı Intent'tir, yani başlatılan Etkinlik'tir.

  • Ancak derin bağlantı birçok biçimde olabilir.

  • Örneğin, bir test için manuel olarak oluşturmak veya Kotlin Multiplatform desteğinizden yararlanmak isteyebilirsiniz.

  • Bu nedenle, derin bağlantı girişini KMP uyumlu bir DeepLinkRequest sınıfına kapsülledik.

  • Android'de ise Android Intent'i alan bir DeepLinkRequest oluşturucumuz var.

  • Sonraki hedefimiz, bir istek verildiğinde NavKey öğesinin eski yığınınıza eklenmesi gerektiğini nasıl anlayacağımızı bulmaktır.

  • İşte bu noktada DeepLinkMatcher sınıfımız devreye giriyor.

  • Bir istek alır ve eşleşmeyle ilişkili NavKey değerini içeren bir MatchResult döndürür.

  • Özellikle web URI'leri için, belirli bir isteği MatchResult değerine dönüştürmek için gereken tüm mantığı kapsayan bir UriDeepLinkMatcher var.

    val matcher = UriDeepLinkMatcher(
      ...,
      ...
    )
  • UriDeepLinkMatcher'nin aldığı ilk parametre bir URI kalıbıdır.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri(...),
      ...
    )
  • Örneğin, bir ADS uygulaması geliştiriyorsunuz ve ana ekrana derin bağlantı eklemek istiyorsunuz.

  • Burada kalıp tam bir URI'dır ve hem HTTP hem de HTTPS ile otomatik olarak eşleşmesi için HTTP şemasını çıkarabiliriz.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/home"),
      ...
    )
  • Ancak belirli bir reklam oturumuna derin bağlantı oluşturmak istiyorsak bu derin bağlantı, ayıklamamız gereken bilgileri (ör. oturum kimliği) içerir.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      …
    )
  • Bu durumda, kalıbımız derin bağlantıdan çıkarmamız gereken bilgiler için yer tutucu olarak süslü parantezler kullanır.

  • Her türlü girişi sınıf örneklerine dönüştürmek için kullanışlı bir kitaplık olduğu ortaya çıktı. Bu kitaplık, Kotlin Serialization.

  • UriDeepLinkMatcher, giriş URI'sini seri durumdan çıkarmak ve NavKey örneği oluşturmak için Kotlin kod çözücüsünü kullanır.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Bu kod çözücünün hangi sınıfla çalışacağını bilmesi gerekir. Bu nedenle, eşleştiriciye NavKey için bir serileştirici de iletmeniz gerekir.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Kod çözücünün, yer tutucuları anahtarın özellikleriyle doğru şekilde eşleştirmesine yardımcı olmak için yer tutucunun adı, özellik adlarıyla eşleşmelidir.

  • Bu örnekte, özellik adı, oturumun kimlik parametresiyle eşleşen kimliktir.

  • UriDeepLinkMatcher oluşturulduğuna göre gelen derin bağlantıyla bir DeepLinkRequest oluşturmamız gerekiyor.

    // 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)
  • Ardından, eşleştirme işlevi çağrılarak ve isteğimiz iletilerek eşleştirme yapılır.

  • Bu işlem, NavKey değerini içeren ve null değer alabilen bir MatchResult döndürür. Örneğimizde bu değer oturum anahtarıdır. Sonuç null ise varsayılan olarak ana ekrana dönebiliriz.

  • Artık birden fazla derin bağlantı, birden fazla eşleştirici olarak yazılmalıdır: her derin bağlantı için bir eşleştirici.

  • Örneğin, burada oturum anahtarımız için iki URI kalıbı var: biri eski bülten kalıbı, diğeri ise normal web URI'si için.

  • Ayrıca ADS ana ekranına giden bir derin bağlantımız da var. Bu nedenle, bunun için üçüncü bir eşleştirici ekliyoruz.

  • Tüm eşleştiriciler, üzerinde işlem yaptığımız aynı listeye eklenir.

  • Böylece eşleşmemiz, listemizdeki tüm eşleştiriciler üzerinde bir döngü haline gelir. Burada, istekle eşleşen tüm eşleştiricileri bulmak için her birinde eşleşmeyi çağırırız.

  • Bunun için mapNotNull kullanarak eşleşmeyenleri filtreleyebilir ve yalnızca geçerli eşleşmelerin listesini alabiliriz.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
  • Çoğu durumda, listenizde muhtemelen yalnızca bir tane boş olmayan eşleşme olur ancak birden fazla geçerli eşleşmenin olabileceği durumlar da vardır.

  • Örneğin, aynı oturum URI kalıbını yeniden kullanan özel bir açılış konuşması ekranı sunduğumuzu ancak artık kimlik yer tutucusu yerine statik bir açılış konuşması yoluna sahip olduğunu varsayalım.

  • Bu durumda, aynı istek her iki eşleştiriciyle de eşleşebilir: kimliğin keynote'a eşit olduğu oturum anahtarı ve keynote NavKey.

  • MatchResult, Comparable'ı uyguladığı için max işlevini kullanarak listedeki tüm geçerli eşleşmeleri karşılaştırarak en iyi eşleşmeyi bulabiliriz. İstekle eşleşen derin bağlantıların olmadığı durumu ele almak için maxOrNull kullanabiliriz.

    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
    )
  • Kısa bir sunum için statik bir eşleştirici listesi kullanmak mantıklı olabilir. Ancak ekranlarınızın birden fazla modüle yayılabilmesi gibi, bu modüllerin de DI sisteminiz aracılığıyla global bir eşleştirici listesine katkıda bulunmasını bekleriz.

    // 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>>
  • Bu yaklaşımı kullanarak tarif depomuzdaki Hilt, Metro ve Koin tariflerimizi derin bağlantılar içerecek şekilde güncelledik.

Tip güvenli serileştirme ve özel serileştiriciler

  • Clara, UriDeepLinkMatcher uygulamamızın derin bağlantı isteğinden alanları ayıklamak ve bunları gösterdiğimiz oturum sınıfı gibi tür güvenli sınıfımıza dönüştürmek için Kotlin Serialization kullandığını belirtmişti.

  • Burada yazıldığı gibi, bu oldukça basit bir dönüşümdür.

  • URI bir dizedir.

  • Alan bir dizedir.

  • Vay canına, ne kadar kolaymış.

  • Ancak yalnızca dize özelliklerinin ötesine geçmemiz gerektiğini biliyorduk.

  • Kotlin Serialization, bu konuda bize sağlam bir temel sunar.

  • Boole, tam sayı, kayan nokta ve hatta Enum gibi tüm temel türler, derin bağlantı değeri söz konusu türe göre ayrıştırılamazsa eşleşmeyi otomatik olarak başarısız kılar ve null değerini döndürür.

  • Örneğin, oturumumuzun kimlik parametresi dize yerine tamsayı olsaydı tamsayı olarak ayrıştırılamayan bir dize iletildiğinde eşleşme başarısız olurdu.

    // pattern = "http://ads.android.com/session/{id}"
    @Serializable data class Session(val id: Int) : NavKey
  • Benzer şekilde, isteğe bağlı bağımsız değişkenleriniz (ör. web URL'nize eklenen analiz verileri) varsa bunlar, anahtarınızda varsayılan değerleri olan özellikler olabilir.

    // pattern = "http://ads.android.com/session/{id}#referrer={referrer}
    @Serializable data class Session(
      val id: Int,
      val referrer: String? = null,
    ) : NavKey
  • Burada, web URL'nizin yapısını, uygulamanızdaki derin bağlantı eşleşmesini temsil eden tip güvenliği sınıflarından veya sınıflardan ayırmak için çok çaba sarf ettiğimizi göreceksiniz. Bu yapı, farklı bir ekibe ait olabilir.

  • Çünkü bazılarınızın son derece kötü derin bağlantılarla uğraştığını biliyorum.

    // "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
  • Örneğin, derin bağlantınıza Base64 kodlu bir resim eklenmiş olabilir.

  • Örneğin, "Bunu uygulamanızın her URL'sine ekleyelim" diyerek devasa bir JSON blob'u eklemiş olabilirler.

  • Size verilen web URL'sini değiştiremediğiniz ancak URL'den bazı bilgileri çıkarmanız gereken durumlarda artık bir çözüm yolu var.

  • İlk teknik, bu bağımsız değişkenin tam olarak nasıl seri durumdan çıkarılacağını veya bir dizeden tür güvenli mülkünüze nasıl dönüştürüleceğini kontrol etmek için DeepLinkSerializer sınıfımızı genişletmektir.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
  • DeepLinkSerializer, Kotlin serileştirme için yardımcı bir sarmalayıcıdır. Bu sarmalayıcı, serileştirici sınıfının uygulanmasını kolaylaştırır. Bu durumda, seri durumdan çıkarıcı girişi her zaman web URL'nizden bir dizedir.

    public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
  • DeepLinkSerializer uygulamak için üç yöntem yazarsınız.

  • Öncelikle bir dizi adı seçersiniz.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      ...
    }
  • Ad alanı oluşturulması gerekmez ancak Kotlin serileştirmenin, serileştiricinizi ne zaman kullanacağını bilecek kadar benzersiz olması gerekir.

  • Ardından, web URL'nizden bu dizeyi alan ve tür güvenli nesnenizi oluşturmaktan sorumlu olan deserialize işlevini uygulamanız gerekir.

    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)
      }
    
       ...
    
    }
  • Son olarak, tam tersi yaklaşım olan serialize yöntemini uygulayacaksınız. Bu yöntem, aynı anahtar sınıfınız rememberNavBackStack gibi durumlarda kullanılıyorsa tercih edilir.

    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)
      }
    }
  • Şimdi özel serileştiricinizi, biraz özel olan bu sınıfa sahip özelliğe uygulayacaksınız. Web UriDeepLinkMatcher, gerisini halledecek.

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

Özel eşleştiricilerle URL bağımsız değişkenlerini önceden işleme

  • Ancak bu Base64 dizesindeki veya web URL'nize eklenen JSON'daki verilerin yalnızca küçük bir kısmını istiyorsanız ne yapmanız gerekir?

  • Bu durumda, web URL'nizin bir kısmını Kotlin Serialization'a ulaşmadan önce önceden işlemek mantıklı olabilir.

  • Bu durumları yönetmek için UriDeepLinkMatcher sınıfını genişletebilir ve matchArguments yöntemini geçersiz kılabilirsiniz.

  • JSON blob'umuz için yalnızca oturum kimliğini istiyor olabiliriz. Diğer verilerle ilgilenmiyoruz.

  • Bu nedenle alt sınıfımızı oluşturup matchArguments yöntemini geçersiz kılıyoruz.

    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>? {
        ...
    }
  • Girişler, URI yolunuzdan eklenen tüm yer tutucular olan ham dizelerdir.

  • Bu sayede, super.matchArguments hizmetini çağırmadan önce bu değerleri önceden işleyip düzenleyebiliriz.

    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)
    }
  • Burada basitleştirilmiş değerler kullanabiliriz ve Kotlin serileştirmesiyle uğraşmamız gerekmez.

Derin bağlantılar için yapay eski yığınlar oluşturma

  • Teşekkürler, Ian.

  • Bu teknikleri sık sık kullanmanız gerekmeyeceğini umuyoruz ancak eşleştirme davranışını nasıl kontrol edebileceğiniz konusunda çok düşündük.

  • Anladım.

  • Derin bağlantı eşleştiricilerle nasıl NavKey elde edeceğimizi öğrendik. Ancak yalnızca gezinme anahtarı içeren bir eski yığın, en iyi kullanıcı deneyimini sunmayabilir.

  • Bu nedenle, geri düğmesinin doğru şekilde çalışması için yapay bir eski yığın kullanmanız gerekir.

  • Sentetik kısmı, eski yığının kullanıcının hiçbir zaman manuel olarak gitmediği anahtarları içermesinden kaynaklanır ancak manuel olarak gitmiş olsaydı izleyeceği yol budur.

  • Örneğin, başlamak üzere olan bir oturumla ilgili bildirimi tıklayan kullanıcıyı doğrudan o oturuma yönlendirirsiniz. Ancak kullanıcı sistem geri düğmesini tetiklediğinde, başlatıcıya çıkmadan önce ana ekranınıza dönmesini istersiniz.

  • Bu HomeScreen, yapay eski yığının bir parçası olur.

  • Derin bağlantı API'leri için iki rakip gereksinimi dengelemek istedik.

  • Bir yandan, farklı derin bağlantıların veya farklı eşleştiricilerin, sentetik eski yığınlarını oluşturmak için farklı mantıklar kullanmak isteyeceğini biliyoruz.

  • Örneğin, her derin bağlantıdan önce bir HomeScreen eklemek, bir ayar ekranındaki alt ekrana derin bağlantı oluşturmak isteyene kadar işe yarayabilir.

  • Şimdi, bu derin bağlantı için yapay eski yığınınıza ek bir katman eklemeniz gerekir.

  • Doğal olarak bu, eski yığın oluşturucuyu her derin bağlantı eşleştiriciye bağlar.

  • Ancak doğru ekranın gösterilmesine kadar geçen her milisaniyenin size gerçek para kaybettirdiğini de biliyoruz.

  • Bu nedenle her maç sonucu için yapay bir eski yığın hesaplamak istemedik.

  • Sadece en iyi eşleşmeyi istedik.

  • Her eşleştirici için özel mantık istiyor ancak her eşleşme için istekli bir şekilde sentetik eski yığın oluşturmak istemiyorsak ne yapmalıyız?

  • Birkaç başarısız denemeden sonra her iki gereksinimi de dengeleyen bir yaklaşım bulduk.

  • Hangi eşleştiriciyi kullanırsanız kullanın, verilen MatchResult ile eski yığını geç oluşturmak için bir withBackStack çağırarak bu eşleştiriciyi genişletebilirsiniz.

    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)
  • Yani birden fazla eşleştiricimiz varsa her birinin, belirli bir NavKey için uygun eski yığını oluşturmak üzere kendi withBackStack çağrısı olur.

  • Çağırdığınız her eşleştirici withBackStack kullanırsa en temiz kodu elde edersiniz. Bu, size eski yığın eşleştiricilerin bir listesini verir.

  • En iyi eşleşmeyi bulduğunuzda, elde ettiğiniz eşleşme sonucu BackStackMatchResult olur.

  • Bu, MatchResult sınıfının bir alt sınıfıdır ve withBackStack lambda'nız tarafından oluşturulan tüm eski yığın olan backStack adlı yeni bir özellik ekler.

  • Bu katmanlı yaklaşım, eşleştiricilerin içindeki mantığı withBackStack lambda'nızda kullanmak istediğiniz özel mantıktan ayırmamıza olanak tanıdı.

Hızlı tur, özet ve kaynaklar

  • Bu kısa bir konuşma olduğu için maalesef süremizin sonuna geldik. Ancak Navigation 3'te bulunan ve Navigation 3 tariflerimizde bulabileceğiniz diğer özelliklerden birkaçını inceleyebiliriz.

  • Anladım.

  • Bu nedenle, eşleştirme mantığını kısa devre yapmak ve eşleştiriciyi işleme, MIME türüne veya kendi özel filtrelerinize göre hızlı bir şekilde hariç tutmak için herhangi bir eşleştiriciye filtre ekleyebilirsiniz.

  • Bu işlem, URI ayrıştırma veya özel serileştirme (Kotlin Serileştirme) işlemlerinden önce gerçekleşir.

  • Ayrıca, NavKey değerinizi almak için herhangi bir ayrıştırma yapmanız gerekmeyen ancak yine de filtre uygulamak istediğiniz durumlar için özel olarak tasarlanmış bir StaticKeyDeepLinkMatcher da sunuyoruz.

    @Serializable object ImagesKey: NavKey
    
    // custom filter
    val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
    
    val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
  • Bu, JPEG resimleri açabilen bir fotoğraf düzenleme uygulaması gibi yalnızca işlem veya MIME türü derin bağlantıları işlemek için idealdir.

  • İşlem ve MIME türünden bahsetmişken, DeepLinkRequest sınıfınıza herhangi bir özel RequestExtras ekleyebilirsiniz. Özel filtreleriniz ve DeepLinkRequest alt sınıflarınız, mantıklarının bir parçası olarak bunu kullanabilir.

  • Bu sayede A/B testi çerçevelerini veya harici sinyalleri dahil edebilirsiniz.

  • Ayrıca ekstralar için de tür güvenli API'lerimiz var.

    val extras = requestExtras {
        put(SectionExtrasKey, "A")
    }
  • Bu mantığın neredeyse tamamı Nav3'ün ortak KMP kodunda yer alır. Bu sayede, tüm eşleştiricileriniz ve eski yığın mantığınız platformlar arasında paylaşılabilir.

  • Navigation 3'ün derin bağlantıları nasıl desteklediğini daha ayrıntılı bir şekilde incelemek isterseniz Nav3 tariflerimize göz atabilirsiniz.

  • Evet. Ayrıca derin bağlantı API'lerimizi DI tariflerimize ve NowInAndroid adlı örnek uygulamamıza entegre ettik.

  • Kısa konuşmamıza katıldığınız için teşekkür ederiz. İyi yolculuklar dileriz.

  • Teşekkür ederiz.

Kod

Kod snippet'leri

"Derin bağlantı API tasarımı ve URI kalıbı eşleştirme" videosunda 04:41'deki Kotlin snippet'i
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
"Derin bağlantı API tasarımı ve URI kalıbı eşleştirme" videosunda 04:53'teki Kotlin snippet'i
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
"Derin bağlantı API tasarımı ve URI kalıbı eşleştirme" videosunda 05:04'teki Kotlin snippet'i
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
"Derin bağlantı API tasarımı ve URI kalıbı eşleştirme" videosunda 05:12'deki Kotlin snippet'i
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  …
)
"Derin bağlantı API tasarımı ve URI kalıbı eşleştirme" videosunda 05:39'daki Kotlin snippet'i
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
"Derin bağlantı API tasarımı ve URI kalıbı eşleştirme" videosunda 05:48'deki Kotlin snippet'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>()
)
"Derin bağlantı API tasarımı ve URI kalıbı eşleştirme" videosunda 06:12'deki Kotlin snippet'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)
"Derin bağlantı API tasarımı ve URI kalıbı eşleştirme" videosunda 07:11'deki Kotlin snippet'i
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}
"Derin bağlantı API tasarımı ve URI kalıbı eşleştirme" videosunda 07:48'deki Kotlin snippet'i
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
)
"Derin bağlantı API tasarımı ve URI kalıbı eşleştirme" videosunda 08:04'teki Kotlin snippet'i
// 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>>
"Type-safe deserialization and custom serializers" (Tür güvenli seri durumdan çıkarma ve özel serileştiriciler) videosunda 09:15'teki Kotlin snippet'i
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
"Type-safe deserialization and custom serializers" (Tip güvenli seri durumdan çıkarma ve özel serileştiriciler) videosunda 09:27'deki Kotlin snippet'i
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
"Type-safe deserialization and custom serializers" (Tip güvenli seri durumdan çıkarma ve özel serileştiriciler) videosunda 09:53'teki Kotlin snippet'i
// "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
"Type-safe deserialization and custom serializers" (Tür güvenli seri durumdan çıkarma ve özel serileştiriciler) videosunda 10:19'daki Kotlin snippet'i
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}
"Type-safe deserialization and custom serializers" (Tür güvenli seri durumdan çıkarma ve özel serileştiriciler) videosunda 10:32'deki Kotlin snippet'i
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
"Type-safe deserialization and custom serializers" (Tip güvenli seri durumdan çıkarma ve özel serileştiriciler) videosunda 10:51'deki Kotlin snippet'i
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
"Type-safe deserialization and custom serializers" (Tür güvenli seri durumdan çıkarma ve özel serileştiriciler) videosunda 11:01'deki Kotlin snippet'i
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)
  }

   ...

}
"Type-safe deserialization and custom serializers" (Tip güvenli seri durumdan çıkarma ve özel serileştiriciler) videosunda 11:13'teki Kotlin snippet'i
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)
  }
}
"Type-safe deserialization and custom serializers" (Tip güvenli seri durumdan çıkarma ve özel serileştiriciler) videosunda 11:24'teki Kotlin snippet'i
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}

@Serializable data class Session(
  val id: String,
  @Serializable(BitmapSerializer::class)
  val placeholderImage: Bitmap? = null
) : NavKey
"Preprocessing URL arguments with custom matchers" (Özel eşleştiricilerle URL bağımsız değişkenlerini önceden işleme) videosunda 12:09'daki Kotlin snippet'i
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>? {
    ...
}
"Preprocessing URL arguments with custom matchers" (Özel eşleştiricilerle URL bağımsız değişkenlerini önceden işleme) videosunda 12:21'deki Kotlin snippet'i
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)
}
"Building synthetic back stacks for deep links" (Derin bağlantılar için sentetik eski yığınlar oluşturma) videosunda 14:31'deki Kotlin snippet'i
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)
"Lightning round, summary, and resources" (Hızlı tur, özet ve kaynaklar) bölümünde 15:52'deki Kotlin snippet'i
@Serializable object ImagesKey: NavKey

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

val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
"Lightning round, summary, and resources" (Hızlı tur, özet ve kaynaklar) bölümünde 16:24'teki Kotlin snippet'i
val extras = requestExtras {
    put(SectionExtrasKey, "A")
}