پایه برنامه‌ها

ساختن پیوندهای عمیق ایمن ازنظر نوع با Jetpack Navigation3

بلندگو

  • Clara Fok

    مهندس نرم‌افزار

  • ایان لیک

    مدیر مهندسی

درباره

درباره سخنرانی

مدیریت پیوندهای عمیق در برنامه‌های Android مدولار می‌تواند به‌راحتی منجر به مسیرهای خراب و غافلگیری‌های زمان اجرا شود.این جلسه معماری جدید پیوند عمیق ایمن ازنظر نوع Jetpack Navigation3 را برای Compose باز می‌کند تا به شما در ساختن یک سیستم مسیریابی قوی و مقیاس‌پذیر کمک کند. نحوه پیاده‌سازی میاناهای برنامه‌سازی کاربردی تطبیق پیوند عمیق آماده، نوشتن سریال‌سازهای سفارشی برای آرگومان‌های پیچیده ایمن ازنظر نوع، و ساختن پشته‌های قبلی‌ها سلسله‌مراتبی پویا را که پیمایش کاربر را قابل‌پیش‌بینی نگه می‌دارند کاوش خواهیم کرد.

خلاصه

  • میاناهای برنامه‌سازی کاربردی پیوند عمیق Navigation 3 درخواست‌های پیوند عمیق ورودی را به کلیدهای پیمایش تبدیل می‌کنند تا پشته قبلی‌های اولیه شما را تعیین کنند. کلاس DeepLinkRequest سازگار با KMP ورودی را کپسوله‌سازی می‌کند، ازجمله سازنده Android Intent.
  • UriDeepLinkMatcher از Kotlin Serialization برای مطابقت دادن الگوهای URI و وازنجیرش آرگومان‌ها به ویژگی‌های NavKey ایمن ازنظر نوع استفاده می‌کند. انواع اولیه به‌طور خودکار کار می‌کنند و اگر مقادیر قابل تجزیه نباشند، مطابقت‌ها ناموفق خواهند بود.
  • توسعه‌دهندگان می‌توانند DeepLinkSerializer را گسترش دهند یا matchArguments را لغو کنند تا رشته‌های پیچیده پیوند عمیق را مدیریت کنند. زیرکلاس‌سازی UriDeepLinkMatcher به شما امکان می‌دهد پیش‌از اجرای «سریال‌سازی Kotlin»، جای‌بان‌های URI خام را پیش‌پردازش کنید.
  • فراخوانی withBackStack در مطابقت‌دهنده‌ها پشته‌های قبلی‌های مصنوعی را به‌صورت تنبل برای بهترین مطابقت ایجاد می‌کند. این کار رفتار صحیح برگشت سیستم را بدون محاسبه پشته‌های قبلی غیرضروری برای هر تطابق معتبر دیگر تضمین می‌کند.

منابع

Gemini Notebook

موضوع

  • jetpack-navigation-3
  • پیوندهای عمیق
  • kotlin-serialization
  • kotlin-multiplatform
  • android-architecture

آوانویسی

مقدمه و پیمایش ۳ دستور پخت

  • سلام، من ایان لیک، مدیر مهندسی تیم معماری Jetpack هستم.

  • و من کلارا فوک، سرپرست فنی Nav 3 هستم.

  • و امروز اینجا هستیم تا درباره ویژگی‌ای صحبت کنیم که برای من بسیار عزیز و مهم است، Nav-- پیوندهای عمیق.

  • درواقع، من آن‌ها را خیلی دوست دارم و پیاده‌سازی را برای هر دو Nav2 و Nav3 به‌عنوان بخشی از نسخه ۱.۲ Nav3 انجام دادم.

  • فکر می‌کنم درواقع سه بار این کار را انجام دادید، چون ما هم در دستورالعمل‌های Nav 3 پیاده‌سازی کردیم.

  • بله، درست است.

  • ما تکرارهای زیادی را روی API انجام دادیم تا مطمئن شویم که با رویکرد Compose-first Nav3 سازگار است.

  • و بخش زیادی از آن با تأکید ما بر دستورالعمل‌ها شروع شد.

  • بله، یکی از اهداف اصلی ما برای Navigation 3 این بود که مزاحم شما نشود.

  • وقتی ویژگی بسیار بلندپروازانه‌ای برای ساختن دارید، نباید برای ممکن کردن آن به درخواست ویژگی از ما نیاز داشته باشید.

  • بنابراین، وقتی این سؤال را دریافت می‌کنیم که «بسیار خوب، چگونه این کار را انجام دهم؟»

  • یا «چگونه حتی به این مشکل نزدیک شوم؟»

  • به‌جای ساختن یک API سفارشی از همان ابتدا، ما یک دستورالعمل می‌سازیم، راهنمایی که توضیح می‌دهد چگونه می‌توانید آن ویژگی را به برنامه خود اضافه کنید بدون اینکه لزوماً نیازی به هیچ تغییری در Navigation 3 باشد.

  • این سبک نشان دادن، نه گفتن، بود که ما را به ایجاد مخزن دستورالعمل‌های Nav3 در GitHub سوق داد.

  • اکنون بیش‌از ۳۵ دستورالعمل برای موارد استفاده رایج مانند پشته‌های قبلی‌ها و برگه‌های پایین دارد که تمام کدهای لازم برای پیاده‌سازی آن در برنامه‌تان را دراختیارتان قرار می‌دهد.

  • این یعنی حتی برای نسخه ۱.۰ Nav3، در نوامبر ۲۰۲۵، ما ازقبل دستورالعملی برای پیاده‌سازی پیوندهای عمیق داشتیم چون می‌دانستیم که این موضوع برای بسیاری از شما مهم است.

  • اما وقتی متوجه می‌شویم که یک دستورالعمل نیاز به داشتن یک دسته از روش‌های کمکی دارد که باید آن‌ها را کپی و در برنامه خود جای‌گذاری کنید، آن زمان است که فرآیندی به نام فارغ‌التحصیلی را در نظر می‌گیریم، جایی که آن کمک‌ها را می‌گیریم و می‌گوییم: «بسیار خوب، اگر آن‌ها بخشی از خود ناوبری سه بودند، واقعاً چگونه به نظر می‌رسیدند؟»

  • این کار تضمین می‌کند که توسعه‌دهندگانی که قبلاً آن کد را کپی و جای‌گذاری کرده‌اند، مشکلی نداشته باشند.

  • می‌توانید آن کد را نگه دارید.

  • اگر راهکارهای سفارشی‌ای دارید که کار می‌کنند، عالی است.

  • هیچ انتقال اجباری‌ای ندارید.

  • اما اگر این مشکل برای شما کاملاً جدید باشد یا نیاز جدیدی داشته باشید، یک راه‌حل آسان و تأییدشده دریافت می‌کنید که به شما امکان می‌دهد به‌نوعی از آن مستقیماً به‌عنوان بخشی از پیمایش استفاده کنید.

  • اما بهترین بخش، برای من شخصاً، این است که پس‌از افزودن آن کمک‌کننده، برمی‌گردیم و دستورالعمل‌هایمان را به‌روز می‌کنیم و آن حس خوب دوپامین را از حذف کد دریافت می‌کنیم.

  • به‌این‌ترتیب، هرکسی که دستورالعمل‌ها را برای رویکرد توصیه‌شده برای آن مشکل بررسی می‌کند، به‌روزترین راه‌حل را خواهد داشت.

  • البته، قضاوت پس‌از وقوع آسان است و این فرایند فارغ‌التحصیلی به ما فرصت می‌دهد تا واقعاً به آن کمک‌کننده‌هایی که در دستورالعمل‌ها ساخته‌ایم نگاه کنیم و بگوییم: «آیا این‌ها واقعاً بهترین میاناهای برنامه‌سازی کاربردی برای آن هستند؟»

  • و در این مورد، این فرصت را به ما می‌دهد که وقتی آن را به کتابخانه منتقل می‌کنیم، این چیزها را بهبود ببخشیم.

  • و این قطعاً درمورد پیوندهای عمیق صدق می‌کرد.

طراحی میانای برنامه‌سازی کاربردی پیوند عمیق و مطابقت الگوی URI

  • متشکرم، ایان.

  • بنابراین، از نظر مفهومی، هم دستورالعمل‌ها و هم میاناهای برنامه‌سازی کاربردی نهایی ما بسیار شبیه هستند، زیرا در Nav 3، شما مالک «پشته قبلی‌ها» هستید.

  • این یعنی تمرکز میاناهای برنامه‌سازی کاربردی پیوند عمیق ما بر این است که مشخص کنیم «پشته قبلی‌ها» اولیه چه باید باشد، به‌ویژه اینکه وقتی کاربران روی پیوند عمیق کلیک می‌کنند تا وارد برنامه شما شوند، چه صفحه‌ای را می‌بینند.

  • اما وقتی کاربر روی آن URI وب کلیک می‌کند واقعاً چه اتفاقی می‌افتد؟

  • اکنون، چه از Nav3 استفاده کنید یا نه، چارچوب Android از شما می‌خواهد که «فیلترهای قصد» را برای هر پیوند عمیقی که برنامه‌تان پشتیبانی می‌کند اعلام کنید.

  • اکنون، جایی که جالب می‌شود این است که وقتی آن پیوند عمیق «فعالیت» شما را راه‌اندازی می‌کند چه اتفاقی می‌افتد.

  • در اینجا، ورودی همان «قصدی» است که برنامه‌تان با آن شروع شده است، همان «فعالیتی» که شروع شده است.

  • اما پیوند عمیق می‌تواند شکل‌های مختلفی داشته باشد.

  • برای مثال، ممکن است بخواهید آن را به‌صورت دستی برای آزمایش بسازید، یا ممکن است بخواهید از پشتیبانی Kotlin Multiplatform خود استفاده کنید.

  • بنابراین، ورودی پیوند عمیق را در کلاس DeepLinkRequest سازگار با KMP کپسوله‌سازی کرده‌ایم.

  • و در Android، سازنده DeepLinkRequest داریم که «هدف Android» را می‌گیرد.

  • سپس، هدف ما این است که بفهمیم، با توجه به درخواست، چگونه بدانیم چه چیزی NavKey باید در «پشته قبلی‌ها» شما قرار گیرد؟

  • اینجا جایی است که کلاس DeepLinkMatcher ما وارد می‌شود.

  • این روش درخواستی را دریافت می‌کند و یک MatchResult را خروجی می‌دهد که حاوی NavKey مرتبط با مطابقت است.

  • به‌طور خاص برای URIهای وب، ما یک UriDeepLinkMatcher داریم که تمام منطق موردنیاز برای تبدیل یک درخواست داده‌شده به MatchResult را دربرمی‌گیرد.

    val matcher = UriDeepLinkMatcher(
      ...,
      ...
    )
  • اکنون، اولین پارامتری که UriDeepLinkMatcher می‌گیرد، الگوی URI است.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri(...),
      ...
    )
  • بنابراین، فرض کنید درحال ساختن برنامه ADS هستید و می‌خواهید پیوند عمیقی به صفحه اصلی اضافه کنید.

  • در اینجا، الگو یک URI دقیق است و می‌توانیم طرح‌واره HTTP را حذف کنیم تا به‌طور خودکار با HTTP و HTTPS مطابقت داشته باشد.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/home"),
      ...
    )
  • اما اگر بخواهیم پیوند عمیق به یک جلسه ADS خاص داشته باشیم، اکنون آن پیوند عمیق حاوی اطلاعاتی است که باید استخراج کنیم، یعنی شناسه جلسه.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      …
    )
  • بنابراین در این مورد، الگوی ما از آکولاد برای عمل کردن به‌عنوان جای‌بان برای اطلاعاتی که باید از پیوند عمیق استخراج کنیم استفاده می‌کند.

  • و مشخص می‌شود که ازقبل کتابخانه مفیدی برای تبدیل هر نوع ورودی به نمونه‌های کلاس وجود دارد که همان Kotlin Serialization است.

  • بنابراین UriDeepLinkMatcher ما از یک رمزگشای Kotlin برای غیرسریالی کردن یک URI ورودی و خروجی یک نمونه NavKey استفاده می‌کند.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • اکنون، آن رمزگشا باید بداند با کدام کلاس کار کند، بنابراین باید یک سریال‌ساز برای NavKey خود به تطبیق‌دهنده ارسال کنید.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • و برای کمک به رمزگشا برای مطابقت دادن صحیح جای‌بان‌ها با ویژگی‌های کلید، نام جای‌بان باید با نام ویژگی‌ها مطابقت داشته باشد.

  • بنابراین در این مثال، نام ویژگی ID است که با پارامتر ID جلسه مطابقت دارد.

  • بسیار خوب، اکنون که UriDeepLinkMatcher را داریم، باید DeepLinkRequest را با پیوند عمیق ورودی بسازیم.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
    
    val request = DeepLinkRequest(
      uri = DeepLinkUri("http://ads.android.com/session/abcd")
    )
    
    val matchResult: UriMatchResult<Session>? = matcher.match(request)
    
    val backStack = listOf(matchResult?.key ?: Home)
  • سپس مطابقت با فراخوانی تابع مطابقت و ارسال درخواست ما انجام می‌شود.

  • این کار به شما یک MatchResult تهی‌پذیر می‌دهد که حاوی NavKey است، که در مثال ما کلید جلسه خواهد بود، یا اگر نتیجه تهی باشد، می‌توانیم به‌طور پیش‌فرض به صفحه اصلی برویم.

  • اکنون، پیوندهای عمیق متعدد باید به‌عنوان تطبیق‌دهنده‌های متعدد نوشته شوند: یک تطبیق‌دهنده برای هر پیوند عمیق.

  • بنابراین برای مثال، در اینجا دو الگوی URI برای کلید نشست خود داریم: یکی برای الگوی خبرنامه قدیمی و دیگری برای URI وب معمولی.

  • و همچنین پیوند عمیقی به صفحه اصلی ADS داریم، بنابراین یک تطبیق‌دهنده سوم برای آن اضافه می‌کنیم.

  • و همه مطابقت‌دهنده‌ها به همان فهرستی اضافه می‌شوند که ما در آن تکرار می‌کنیم.

  • بنابراین اکنون تطابق ما به حلقه‌ای روی همه تطبیق‌دهنده‌ها در فهرست ما تبدیل می‌شود، که در آن برای هرکدام تطابق را فراخوانی می‌کنیم تا همه تطبیق‌دهنده‌هایی را که با درخواست مطابقت دارند پیدا کنیم.

  • برای انجام این کار، می‌توانیم از mapNotNull برای فیلتر کردن موارد غیرمنطبق استفاده کنیم و فهرستی از موارد منطبق معتبر را دریافت کنیم.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
  • اکنون در بیشتر موارد، احتمالاً فقط یک مطابقت غیرتهی در فهرستتان خواهید داشت، اما مواردی وجود دارد که ممکن است بیش‌از یک مطابقت معتبر وجود داشته باشد.

  • برای مثال، فرض کنید یک صفحه سخنرانی کلیدی ویژه معرفی می‌کنیم که از همان الگوی URI جلسه استفاده می‌کند، اما اکنون به‌جای جای‌بان شناسه، یک مسیر سخنرانی کلیدی ثابت دارد.

  • بنابراین در این مورد، همان درخواست می‌تواند با هر دو مطابقت‌دهنده مطابقت داشته باشد: کلید جلسه که در آن شناسه برابر با keynote است و keynote NavKey.

  • بنابراین، ازآنجایی‌که MatchResult Comparable را پیاده‌سازی می‌کند، می‌توانیم با مقایسه همه مطابقت‌های معتبر در فهرست بااستفاده از تابع max، یا درواقع maxOrNull برای رسیدگی به موردی که هیچ پیوند عمیقی با درخواست مطابقت ندارد، بهترین مطابقت را پیدا کنیم.

    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
    )
  • اکنون، یک فهرست ایستا از تطبیق‌دهنده‌ها ممکن است برای یک سخنرانی کوتاه منطقی باشد، اما درست مانند اینکه صفحه‌های شما می‌توانند در چندین ماژول پخش شوند، ما انتظار داریم که همان ماژول‌ها ازطریق سیستم DI شما به یک فهرست جهانی از تطبیق‌دهنده‌ها کمک کنند.

    // Feature A Module
    @ContributesTo(ActivityScope::class)
    @BindingContainer
    object FeatureAModule {
        @IntoSet
        @Provides
        fun provideDeepLinkMatcher() : UriDeepLinkMatcher<NavKey> =
          UriDeepLinkMatcher(
            uriPattern = DeepLinkUri("ads.android.com/featureA"),
            serializer<FeatureBKey>(),
          )
    }
    
    // in MainActivity
    @Inject lateinit var matchers: Set<UriDeepLinkMatcher<NavKey>>
  • و بااستفاده از این رویکرد، دستورالعمل‌های Hilt،‏ Metro، و Koin در مخزن دستورالعمل‌های ما به‌روز شده‌اند تا پیوندهای عمیق را دربرگیرند.

واسازی ایمن ازنظر نوع و سریال‌سازهای سفارشی

  • اکنون، کلارا اشاره کرد که UriDeepLinkMatcher ما از Kotlin Serialization برای استخراج فیلدها از درخواست پیوند عمیق و تبدیل آن به کلاس ایمن ازنظر نوع ما، مانند کلاس جلسه ما که نشان دادیم، استفاده می‌کند.

  • همان‌طور که در اینجا نوشته شده است، این یک تبدیل بسیار ساده است.

  • URI یک رشته است.

  • فیلد رشته است.

  • وای، چقدر آسان بود.

  • اما می‌دانستیم که باید فراتر از ویژگی‌های رشته‌ای برویم.

  • اینجاست که Kotlin Serialization ازقبل پایه محکمی برای کار کردن به ما می‌دهد.

  • همه انواع اولیه، مانند بولی، عدد صحیح، عدد اعشاری، حتی Enums به‌طور پیش‌فرض کار می‌کنند و اگر مقدار پیوند عمیق نتواند به‌عنوان آن نوع خاص تجزیه شود، به‌طور خودکار مطابقت ناموفق می‌شود و مقدار تهی برمی‌گرداند.

  • برای مثال، اگر پارامتر شناسه جلسه ما درواقع عدد صحیح بود نه رشته، ارسال رشته‌ای که نمی‌توانست به‌صورت عدد صحیح تجزیه شود باعث عدم تطابق می‌شد.

    // pattern = "http://ads.android.com/session/{id}"
    @Serializable data class Session(val id: Int) : NavKey
  • به‌طور مشابه، اگر می‌خواستید آرگومان‌های اختیاری داشته باشید، مثلاً برای داده‌های تجزیه‌وتحلیل پیوست‌شده به نشانی وب شما، این آرگومان‌ها می‌توانند ویژگی‌هایی در کلید شما باشند که مقادیر پیش‌فرض دارند.

    // pattern = "http://ads.android.com/session/{id}#referrer={referrer}
    @Serializable data class Session(
      val id: Int,
      val referrer: String? = null,
    ) : NavKey
  • در اینجا متوجه خواهید شد که ما واقعاً تلاش کرده‌ایم ساختار نشانی وب شما را (که ممکن است متعلق به تیم دیگری باشد) از کلاس‌های ایمن تایپی یا کلاس‌هایی که نشان‌دهنده مطابقت پیوند عمیق در برنامه شما هستند جدا کنیم.

  • چون می‌دانم برخی‌از شما با پیوندهای عمیق واقعاً بدی سروکار دارید.

    // "id={id}&base64={placeholderImage}"
    @Serializable data class Session(
      val id: String,
      val placeholderImage: Bitmap? = null
    ) : NavKey
    
    // "jsonBlob={user}"
    @Serializable data class Profile(
      val user: User,
    ) : NavKey
  • مثلاً شاید تصویری با کدبندی base64 به پیوند عمیق شما پیوست شده باشد.

  • مثلاً یک حباب JSON بزرگ که تصمیم گرفته‌اند «اوه، بیایید آن را در هر نشانی وب برنامه شما قرار دهیم.»

  • اکنون راهی برای پیشرفت وجود دارد، حتی زمانی که نمی‌توانید نشانی وب داده‌شده را تغییر دهید، اما همچنان باید اطلاعاتی را از آن استخراج کنید.

  • بنابراین، اولین تکنیک این است که کلاس DeepLinkSerializer خود را گسترش دهیم تا دقیقاً کنترل کنیم که چگونه آن آرگومان از یک رشته به ویژگی ایمن ازنظر نوع شما تبدیل یا وازنجیرش می‌شود.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
  • ‫DeepLinkSerializer یک پوشش کمکی در اطراف Kotlin Serialization است که پیاده‌سازی کلاس سریال‌ساز آن‌ها را ساده می‌کند، زیرا در این مورد، ورودی سریال‌زدا همیشه فقط یک رشته از نشانی وب شما است.

    public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
  • اکنون برای پیاده‌سازی DeepLinkSerializer، سه روش می‌نویسید.

  • ابتدا نام سریال را انتخاب می‌کنید.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      ...
    }
  • نیازی به فضای نام ندارد، اما باید به‌اندازه کافی منحصربه‌فرد باشد تا Kotlin Serialization بداند چه زمانی از سریال‌ساز شما استفاده کند.

  • سپس باید deserialize را پیاده‌سازی کنید که آن رشته را از نشانی وب شما دریافت می‌کند و مسئول ساختن شیء ایمن ازنظر نوع شما است.

    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)
      }
    
       ...
    
    }
  • درنهایت، serialize را پیاده‌سازی خواهید کرد که رویکردی مخالف است و اگر کلاس کلید یکسان شما در مواردی مثل 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)
      }
    }
  • Now you'll apply your custom serializer to the property that has that class that's a little special, and web UriDeepLinkMatcher will take care of the rest.

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

پیش‌پردازش آرگومان‌های نشانی وب با تطبیق‌دهنده‌های سفارشی

  • اما اگر فقط کمی از داده‌های آن رشته پایه ۶۴ یا آن JSON که به نشانی وب شما پیوست شده است را بخواهید چه؟

  • در این صورت، ممکن است منطقی باشد که برخی‌از نشانی‌های وب را قبل‌از اینکه به Kotlin Serialization برسند پیش‌پردازش کنید.

  • برای رسیدگی به این موارد، می‌توانید UriDeepLinkMatcher را گسترش دهید و روش matchArguments آن را لغو کنید.

  • اکنون، شاید برای حباب JSON ما، تنها چیزی که واقعاً می‌خواهیم فقط شناسه جلسه باشد، مثلاً واقعاً به بقیه آن اهمیتی نمی‌دهیم.

  • بنابراین زیرکلاس خود را ایجاد می‌کنیم و matchArguments را لغو می‌کنیم.

    class SessionUriDeepLinkMatcher(
      uriPattern: DeepLinkUri
    ) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {
    
      override fun matchArguments(
        pathArgs: Map<String, List<String>>,
        queryArgs: Map<String, List<String>>,
        fragmentArgs: Map<String, List<String>>
      ): UriMatchResult<Session>? {
        ...
    }
  • ورودی‌ها همان رشته‌های خام هستند، همه جای‌بان‌هایی که از مسیر URI شما اضافه شده‌اند.

  • و این به ما امکان می‌دهد تا قبل‌از فراخوانی super.matchArguments، آن مقادیر را پیش‌پردازش و ویرایش کنیم.

    class SessionUriDeepLinkMatcher(
      uriPattern: DeepLinkUri
    ) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {
    
      override fun matchArguments(
        pathArgs: Map<String, List<String>>,
        queryArgs: Map<String, List<String>>,
        fragmentArgs: Map<String, List<String>>
      ): UriMatchResult<Session>? {
        val finalQueryArgs = buildMap {
          putAll(queryArgs)
          val jsonBlob = remove("session")?.firstOrNull()
          if (jsonBlob != null) { put("session", extractSessionIdFromJson(jsonBlob)) }
        }
        return super.matchArguments(pathArgs, finalQueryArgs, fragmentArgs)
    }
  • اکنون می‌توانیم مقادیر ساده‌شده‌ای در اینجا داشته باشیم و اصلاً نیازی نیست با Kotlin Serialization سروکله بزنیم.

ساختن پشته قبلی‌های مصنوعی برای پیوندهای عمیق

  • متشکرم، ایان.

  • امیدواریم که مجبور نباشید اغلب به این تکنیک‌ها مراجعه کنید، اما ما فکر زیادی درباره نحوه کنترل رفتار مطابقت کرده‌ایم.

  • بسیارخوب.

  • اکنون می‌دانیم که چگونه با تطبیق‌دهنده‌های پیوند عمیق NavKey را دریافت کنیم، اما پشته قبلی‌ها با فقط کلید پیمایش ممکن است بهترین تجربه کاربری را ارائه ندهد.

  • بنابراین، برای اطمینان از رفتار مناسب برگشت، به یک پشته قبلی‌ها مصنوعی نیاز دارید.

  • بخش مصنوعی از این واقعیت ناشی می‌شود که پشته قبلی‌ها حاوی کلیدهایی است که کاربر هرگز به‌صورت دستی به آن‌ها پیمایش نکرده است، اما این مسیری است که اگر به‌صورت دستی به آنجا پیمایش می‌کرد، طی می‌کرد.

  • بنابراین، برای مثال، با کلیک کردن روی اعلان جلسه‌ای که درحال شروع شدن است، کاربر را مستقیماً به آن جلسه می‌برید، اما وقتی کاربر دکمه برگشت سیستم را فعال می‌کند، احتمالاً می‌خواهید قبل‌از خروج به راه‌انداز، به HomeScreen شما برگردد.

  • آن HomeScreen بخشی از Synthetic Back Stack می‌شود.

  • اکنون برای میاناهای برنامه‌سازی کاربردی پیوند عمیق، می‌خواستیم رقابت کنیم و می‌خواستیم بین دو الزام رقابتی توازن برقرار کنیم.

  • ازیک‌سو، می‌دانیم که پیوندهای عمیق مختلف یا تطبیق‌دهنده‌های مختلف، منطق متفاوتی برای ساختن پشته قبلی‌های مصنوعی خود می‌خواهند.

  • بنابراین برای مثال، افزودن HomeScreen قبل‌از هر پیوند عمیق ممکن است تا زمانی‌که بخواهید پیوند عمیق به زیرصفحه‌ای در صفحه تنظیمات ایجاد کنید کار کند.

  • اکنون، برای آن پیوند عمیق خاص به یک لایه اضافی در پشته قبلی‌های مصنوعی نیاز دارید.

  • طبیعتاً این امر سازنده پشته قبلی‌ها را به هر تطبیق‌دهنده پیوند عمیق مرتبط می‌کند.

  • اما ازسوی دیگر، می‌دانیم که هر میلی‌ثانیه‌ای که طول می‌کشد تا صفحه درست نمایش داده شود، برای شما هزینه واقعی دارد.

  • بنابراین نمی‌خواستیم برای هر نتیجه منطبق، پشته قبلی‌های مصنوعی محاسبه کنیم.

  • ما فقط بهترین تطابق را می‌خواستیم.

  • بنابراین، اگر منطق سفارشی برای هر تطبیق‌دهنده بخواهیم، اما نخواهیم برای هر تطبیق یک پشته قبلی‌های مصنوعی ایجاد کنیم، چه کاری باید انجام دهیم؟

  • بنابراین پس‌از چند شروع ناموفق، به رویکردی رسیدیم که هر دو نیاز را برآورده می‌کند.

  • مهم نیست چه نوع تطبیق‌دهنده‌ای دارید، می‌توانید آن تطبیق‌دهنده خاص را با فراخوانی withBackStack تقویت کنید تا پشته قبلی‌ها را با MatchResult داده‌شده به‌صورت تنبل بسازید.

    val homeMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
      listOf(Home, matchResult.key)
    }
    
    val sessionMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
      listOf(Home, SessionList, matchResult.key)
    }
    
    val backStackMatchers: List<BackStackMatcher<NavKey, NavKey>> = listOf(
      homeMatcher, sessionMatcher
    )
    
    val matchResults = backStackMatchers.mapNotNull { matcher.match(request) }
    val bestMatch: BackStackMatchResult<NavKey, NavKey>? = matchResults.maxOrNull()
    
    val backStack = bestMatch?.backStack ?: listOf(Home)
  • بنابراین، اگر چند تطبیق‌دهنده داشته باشیم، هرکدام فراخوانی withBackStack خود را برای ساختن پشته قبلی‌ها مناسب برای آن NavKey خاص خواهند داشت.

  • اگر هر تطبیق‌دهنده‌ای که فراخوانی می‌کنید از withBackStack استفاده کند، تمیزترین کد را دریافت خواهید کرد که فهرستی از تطبیق‌دهنده‌های پشته قبلی‌ها را به شما می‌دهد.

  • و وقتی بهترین انطباق را پیدا می‌کنید، نتیجه انطباقی که دریافت می‌کنید BackStackMatchResult است.

  • این یک زیرکلاس از MatchResult است که ویژگی جدیدی به نام backStack اضافه می‌کند، که کل پشته قبلی‌ها است که توسط لامبدا withBackStack شما تولید شده است.

  • این رویکرد لایه‌بندی به ما امکان می‌دهد منطق داخل تطبیق‌دهنده‌ها را از هر منطق سفارشی که می‌خواهید در داخل لامبدا withBackStack خود استفاده کنید، جدا کنیم.

دور سریع، خلاصه، و منابع

  • خب، این یک سخنرانی سریع است، بنابراین متأسفانه وقت ما تقریباً تمام شده است، اما شاید بتوانیم چند ویژگی دیگر را که در ناوبری سه داریم و در دستورالعمل‌های ناوبری سه ما پیدا خواهید کرد، مرور کنیم.

  • بسیارخوب.

  • بنابراین، می‌توانید فیلترهایی را به هر تطبیق‌دهنده اضافه کنید تا منطق تطبیق را کوتاه کنید و به‌سرعت تطبیق‌دهنده را براساس کنش، نوع MIME، یا فیلترهای سفارشی خودتان مستثنی کنید.

  • و این اتفاق قبل‌از انجام هرگونه تجزیه URI یا سریال‌سازی سفارشی، «سریال‌سازی Kotlin»، رخ می‌دهد.

  • ما همچنین یک StaticKeyDeepLinkMatcher ارائه می‌دهیم، به‌ویژه برای مواردی که نیازی به انجام هیچ‌گونه تجزیه برای دریافت NavKey خود ندارید، اما همچنان می‌خواهید فیلترها را اعمال کنید.

    @Serializable object ImagesKey: NavKey
    
    // custom filter
    val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
    
    val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
  • این برای مدیریت پیوندهای عمیق فقط کنش یا نوع MIME عالی است، مانند یک برنامه ویرایش عکس که می‌تواند باز کردن تصاویر JPEG را مدیریت کند.

  • و درباره کنش و نوع MIME، می‌توانید هر RequestExtras سفارشی را به DeepLinkRequest خود پیوست کنید، که فیلترهای سفارشی و زیرکلاس‌های DeepLinkRequest شما می‌توانند به‌عنوان بخشی از منطق خود استفاده کنند.

  • این به شما امکان می‌دهد چارچوب‌های آزمایش AB یا سیگنال‌های خارجی را ترکیب کنید.

  • و همچنین یک میانای برنامه‌سازی کاربردی کاملاً ایمن ازنظر نوع برای موارد اضافی داریم.

    val extras = requestExtras {
        put(SectionExtrasKey, "A")
    }
  • اوه، و تقریباً همه این منطق در کد KMP مشترک Nav3 قرار دارد، بنابراین همه منطق‌های تطبیق‌دهنده و پشته قبلی‌ها می‌توانند در پلاتفرم‌های مختلف هم‌رسانی شوند.

  • بنابراین، اگر می‌خواهید با حوصله بیشتری نحوه پشتیبانی Navigation 3 از پیوندهای عمیق را بررسی کنید، دستورالعمل‌های Nav3 ما را ببینید.

  • بله، و همچنین میاناهای برنامه‌سازی کاربردی پیوند عمیق خود را در دستورالعمل‌های DI و همچنین برنامه قهرمان خود، NowInAndroid، ادغام کرده‌ایم.

  • از اینکه به سخنرانی کوتاه ما آمدید سپاس‌گزاریم، و امیدواریم از پیمایش لذت ببرید.

  • متشکریم.

کد

تکه‌کدها

قطعه Kotlin در ۰۴:۴۱ در «طراحی میانای برنامه‌سازی کاربردی پیوند عمیق و تطبیق الگوی URI»
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
قطعه Kotlin در ۰۴:۵۳ در «طراحی میانای برنامه‌سازی کاربردی پیوند عمیق و تطبیق الگوی URI»
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
قطعه Kotlin در ۰۵:۰۴ در «طراحی Deep link API و تطابق الگوی URI»
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
قطعه Kotlin در ۰۵:۱۲ در «طراحی میانای برنامه‌سازی کاربردی پیوند عمیق و تطبیق الگوی شناسه منبع یکسان»
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  …
)
قطعه Kotlin در 05:39 در «طراحی میانای برنامه‌سازی کاربردی پیوند عمیق و تطبیق الگوی URI»
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
قطعه Kotlin در ۰۵:۴۸ در «طراحی میانای برنامه‌سازی کاربردی پیوند عمیق و تطبیق الگوی URI»
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
قطعه Kotlin در ۰۶:۱۲ در «طراحی میانای برنامه‌سازی کاربردی پیوند عمیق و تطبیق الگوی URI»
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)

val request = DeepLinkRequest(
  uri = DeepLinkUri("http://ads.android.com/session/abcd")
)

val matchResult: UriMatchResult<Session>? = matcher.match(request)

val backStack = listOf(matchResult?.key ?: Home)
قطعه Kotlin در 07:11 در «طراحی میانای برنامه‌سازی کاربردی پیوند عمیق و تطبیق الگوی URI»
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}
قطعه Kotlin در ۰۷:۴۸ در «طراحی میانای برنامه‌سازی کاربردی پیوند عمیق و تطبیق الگوی 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
)
قطعه Kotlin در 08:04 در «طراحی میانای برنامه‌سازی کاربردی پیوند عمیق و تطبیق الگوی URI»
// Feature A Module
@ContributesTo(ActivityScope::class)
@BindingContainer
object FeatureAModule {
    @IntoSet
    @Provides
    fun provideDeepLinkMatcher() : UriDeepLinkMatcher<NavKey> =
      UriDeepLinkMatcher(
        uriPattern = DeepLinkUri("ads.android.com/featureA"),
        serializer<FeatureBKey>(),
      )
}

// in MainActivity
@Inject lateinit var matchers: Set<UriDeepLinkMatcher<NavKey>>
قطعه Kotlin در ۰۹:۱۵ در «Type-safe deserialization and custom serializers»
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
قطعه Kotlin در ۰۹:۲۷ در «واسازی ایمن تایپی و سریال‌سازهای سفارشی»
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
قطعه Kotlin در 09:53 در «Type-safe deserialization and custom serializers»
// "id={id}&base64={placeholderImage}"
@Serializable data class Session(
  val id: String,
  val placeholderImage: Bitmap? = null
) : NavKey

// "jsonBlob={user}"
@Serializable data class Profile(
  val user: User,
) : NavKey
قطعه Kotlin در 10:19 در «واسازی ایمن ازنظر نوع و سریال‌سازهای سفارشی»
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}
قطعه Kotlin در 10:32 در «واسازی ایمن ازنظر نوع و سریال‌سازهای سفارشی»
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
قطعه Kotlin در ۱۰:۵۱ در «واسازی نوع ایمن و سریال‌سازهای سفارشی»
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
قطعه کد Kotlin در دقیقه ۱۱:۰۱ در «واسازی ایمن تایپی و سریال‌سازهای سفارشی»
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  override fun deserialize(value: String): Bitmap {
    val imageBytes = Base64.decode(value, Base64.DEFAULT)
   // This better be a small image…
    return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
  }

   ...

}
قطعه Kotlin در ۱۱:۱۳ در «Type-safe deserialization and custom serializers»
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {

  ...

  override fun serialize(value: Bitmap): String {
    val byteArrayOutputStream = ByteArrayOutputStream()
    // Compress the bitmap into the output stream (e.g., JPEG or PNG)
    value.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
    val byteArray = byteArrayOutputStream.toByteArray()
    // Encode the byte array to a Base64 string
    return Base64.encodeToString(byteArray, Base64.DEFAULT)
  }
}
قطعه Kotlin در ۱۱:۲۴ در «Type-safe deserialization and custom serializers»
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}

@Serializable data class Session(
  val id: String,
  @Serializable(BitmapSerializer::class)
  val placeholderImage: Bitmap? = null
) : NavKey
قطعه Kotlin در ۱۲:۰۹ در «پیش‌پردازش آرگومان‌های نشانی وب با تطبیق‌دهنده‌های سفارشی»
class SessionUriDeepLinkMatcher(
  uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {

  override fun matchArguments(
    pathArgs: Map<String, List<String>>,
    queryArgs: Map<String, List<String>>,
    fragmentArgs: Map<String, List<String>>
  ): UriMatchResult<Session>? {
    ...
}
قطعه Kotlin در ۱۲:۲۱ در «پیش‌پردازش آرگومان‌های نشانی وب با تطبیق‌دهنده‌های سفارشی»
class SessionUriDeepLinkMatcher(
  uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {

  override fun matchArguments(
    pathArgs: Map<String, List<String>>,
    queryArgs: Map<String, List<String>>,
    fragmentArgs: Map<String, List<String>>
  ): UriMatchResult<Session>? {
    val finalQueryArgs = buildMap {
      putAll(queryArgs)
      val jsonBlob = remove("session")?.firstOrNull()
      if (jsonBlob != null) { put("session", extractSessionIdFromJson(jsonBlob)) }
    }
    return super.matchArguments(pathArgs, finalQueryArgs, fragmentArgs)
}
قطعه Kotlin در ۱۴:۳۱ در «ساخت پشته‌های قبلی‌ها مصنوعی برای پیوندهای عمیق»
val homeMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
  listOf(Home, matchResult.key)
}

val sessionMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
  listOf(Home, SessionList, matchResult.key)
}

val backStackMatchers: List<BackStackMatcher<NavKey, NavKey>> = listOf(
  homeMatcher, sessionMatcher
)

val matchResults = backStackMatchers.mapNotNull { matcher.match(request) }
val bestMatch: BackStackMatchResult<NavKey, NavKey>? = matchResults.maxOrNull()

val backStack = bestMatch?.backStack ?: listOf(Home)
قطعه Kotlin در ۱۵:۵۲ در «دور رعدآسا، خلاصه، و منابع»
@Serializable object ImagesKey: NavKey

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

val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
قطعه Kotlin در ۱۶:۲۴ در «Lightning round, summary, and resources»
val extras = requestExtras {
    put(SectionExtrasKey, "A")
}