أساسيات التطبيقات

إنشاء روابط عميقة تمنع أخطاء الكتابة باستخدام Jetpack Navigation3

سماعات

  • كلارا فوك

    مهندس برمجيات

  • إيان ليك

    مدير هندسي

لمحة

لمحة عن المحاضرة

قد تؤدي إدارة الربط العميق في تطبيقات Android التي تتألف من وحدات إلى حدوث مشاكل في المسارات وأخطاء غير متوقعة أثناء وقت التشغيل.تستعرض هذه الجلسة بنية الربط العميق الجديدة في Jetpack Navigation3 التي تمنع أخطاء الكتابة في Compose لمساعدتك في إنشاء نظام توجيه قوي وقابل للتوسّع. سنستكشف كيفية تنفيذ واجهات برمجة تطبيقات مطابقة الروابط العميقة الجاهزة للاستخدام، وكتابة أدوات تسلسل مخصّصة للوسيطات المعقّدة التي تمنع أخطاء الكتابة، وإنشاء أنشطة سابقة هرمية ديناميكية تحافظ على إمكانية توقّع تنقّل المستخدم.

ملخّص

  • تحوّل واجهات برمجة التطبيقات الخاصة بالروابط لصفحات معيّنة في Navigation 3 طلبات الروابط لصفحات معيّنة الواردة إلى مفاتيح تنقّل لتحديد الأنشطة السابقة الأولية. تتضمّن فئة DeepLinkRequest المتوافقة مع KMP الإدخال، بما في ذلك دالة إنشاء Android Intent.
  • يستخدم UriDeepLinkMatcher تسلسل Kotlin لمطابقة أنماط معرّفات الموارد المنتظمة (URI) وإلغاء تسلسل الوسيطات في سمات NavKey التي تمنع أخطاء الكتابة. تعمل الأنواع الأساسية تلقائيًا، وتتعذّر المطابقة إذا تعذّر تحليل القيم.
  • يمكن للمطوّرين توسيع نطاق DeepLinkSerializer أو إلغاء matchArguments للتعامل مع سلاسل الروابط العميقة المعقّدة. تتيح لك الفئة الفرعية UriDeepLinkMatcher معالجة العناصر النائبة لـ URI الأوّلي مسبقًا قبل تشغيل Kotlin Serialization.
  • يؤدي استدعاء withBackStack في أدوات المطابقة إلى إنشاء أنشطة سابقة اصطناعية بشكل كسول للحصول على أفضل مطابقة. يضمن ذلك سلوك الرجوع الصحيح للنظام بدون احتساب الأنشطة السابقة غير الضرورية لكل مطابقة صالحة أخرى.

الموارد

Gemini Notebook

المواضيع

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

النص

تنزيل النص

مقدمة والتنقّل في 3 وصفات

  • مرحبًا، أنا "إيان ليك"، مدير الهندسة في فريق Jetpack Architecture.

  • وأنا "كلارا فوك"، المسؤولة التقنية عن Nav 3.

  • واليوم، نحن هنا لنتحدث عن ميزة قريبة وعزيزة على قلبي، وهي الروابط العميقة.

  • في الواقع، لقد أعجبتني كثيرًا لدرجة أنّني نفّذت كلّاً من Nav2 وNav3 كجزء من الإصدار 1.2 من Nav3.

  • أعتقد أنّك نفّذت ذلك ثلاث مرات، لأنّنا نفّذنا أيضًا عملية تنفيذ في وصفات Nav 3 أيضًا.

  • نعم، هذا صحيح.

  • لقد أجرينا الكثير من التعديلات على واجهة برمجة التطبيقات للتأكّد من أنّها تتوافق مع نهج Nav3 الذي يركّز على Compose.

  • وبدأ الكثير من ذلك بتركيزنا على الوصفات.

  • نعم، كان أحد أهدافنا الرئيسية في Navigation 3 هو عدم إزعاجك.

  • عندما يكون لديك ميزة طموحة بشكل خاص لإنشائها، لا ينبغي أن يتطلب الأمر طلب ميزة منا لجعلها ممكنة بالفعل.

  • لذا، عندما نتلقى هذا السؤال، "حسنًا، كيف أفعل هذا؟"

  • أو "كيف يمكنني التعامل مع هذه المشكلة؟"

  • بدلاً من إنشاء واجهة برمجة تطبيقات مخصّصة من البداية، ننشئ وصفة، وهي دليل يشرح كيفية إضافة هذه الميزة إلى تطبيقك بدون الحاجة بالضرورة إلى إجراء أي تغييرات في Navigation 3 على الإطلاق.

  • كان هذا الأسلوب في العرض، وليس الشرح، هو الذي دفعنا إلى إنشاء مستودع وصفات Nav3 على GitHub.

  • وتتضمّن الآن أكثر من 35 وصفة لحالات الاستخدام الشائعة، مثل الأنشطة السابقة المتعدّدة والبطاقات السفلية التي تمنحك كل الرموز التي تحتاجها لتنفيذها في تطبيقك.

  • هذا يعني أنّه حتى بالنسبة إلى الإصدار 1.0 من Nav3، الذي تم إطلاقه في نوفمبر 2025، كان لدينا بالفعل طريقة لتنفيذ الربط العميق لأنّنا كنا نعلم أنّ ذلك مهم للكثير منكم.

  • ولكن عندما ندرك أنّ الوصفة تتطلّب مجموعة من الطرق المساعدة التي يجب نسخها ولصقها في تطبيقك، نلجأ إلى عملية تُسمّى "التخرج"، حيث نأخذ هذه الطرق المساعدة ونقول: "حسنًا، كيف ستبدو هذه الطرق إذا كانت جزءًا من الإصدار الثالث من Navigation نفسه؟"

  • يضمن ذلك عدم حدوث أي مشاكل للمطوّرين الذين سبق لهم نسخ هذا الرمز ولصقه.

  • يمكنك الاحتفاظ بهذا الرمز.

  • إذا كانت لديك حلول مخصّصة تعمل بشكل جيد، فهذا أمر رائع.

  • ليس لديك أي عملية نقل بيانات إلزامية.

  • ولكن إذا كانت المشكلة جديدة بالنسبة إليك أو كانت متطلبات جديدة، ستحصل على حل سهل وموثوق يتيح لك استخدامه مباشرةً كجزء من عملية التنقّل.

  • لكن الجزء الأفضل، بالنسبة لي شخصيًا، هو أنه بعد إضافة هذه الدالة المساعدة، نعود ونحدّث وصفاتنا ونحصل على هذا الشعور الرائع من الدوبامين عند حذف الرمز.

  • بهذه الطريقة، سيحصل أي شخص يبحث عن وصفات للطريقة المقترَحة لحلّ هذه المشكلة على أحدث الحلول.

  • بالطبع، من السهل تقييم الأمور بعد حدوثها، وتمنحنا عملية الترقية هذه فرصة لإلقاء نظرة على تلك الأدوات المساعدة التي أنشأناها في الوصفات والتساؤل: "هل هذه هي أفضل واجهات برمجة تطبيقات لذلك؟"

  • وفي هذه الحالة، يمنحنا ذلك فرصة لتحسين هذه الأمور أثناء نقلها إلى المكتبة.

  • وهذا بالتأكيد هو الحال بالنسبة للروابط العميقة.

تصميم واجهة برمجة التطبيقات للروابط لصفحات معيّنة ومطابقة أنماط معرّفات الموارد المنتظمة (URI)

  • شكرًا لك يا "إيان".

  • لذا، من الناحية النظرية، تتشابه كل من الوصفات وواجهات برمجة التطبيقات النهائية إلى حد كبير، لأنّك في Nav 3 تمتلك الأنشطة السابقة.

  • هذا يعني أنّ تركيز واجهات برمجة التطبيقات للروابط لصفحات معيّنة ينصبّ على تحديد ما يجب أن تكون عليه الأنشطة السابقة الأولية، وتحديدًا الشاشة التي تظهر للمستخدمين عند النقر على رابط لصفحة معيّنة للدخول إلى تطبيقك.

  • ولكن ماذا يحدث فعليًا عندما ينقر المستخدم على معرّف الموارد المنتظم (URI) للويب؟

  • والآن، سواء كنت تستخدم Nav3 أم لا، يتطلّب إطار عمل Android أن تعلن عن intent filter لكل رابط عميق يتوافق معه تطبيقك.

  • الآن، ما يثير الاهتمام هو ما يحدث بمجرد أن يؤدي هذا الرابط العميق إلى بدء نشاطك.

  • هنا، الإدخال هو Intent الذي يبدأ به تطبيقك، أي Activity الذي بدأ.

  • ولكن يمكن أن يتخذ الرابط العميق أشكالاً عديدة.

  • على سبيل المثال، قد تحتاج إلى إنشائه يدويًا لإجراء اختبار، أو قد تحتاج إلى الاستفادة من دعم Kotlin Multiplatform.

  • لذلك، قمنا بتضمين إدخال الرابط العميق في فئة DeepLinkRequest متوافقة مع KMP.

  • وعلى Android، لدينا دالة إنشاء DeepLinkRequest تستخدم Android Intent.

  • بعد ذلك، هدفنا هو معرفة كيفية تحديد ما يجب وضعه في الأنشطة السابقة 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) عادي للويب.

  • ولدينا أيضًا رابط لصفحة في التطبيق يؤدي إلى الشاشة الرئيسية لخدمة "إعلانات Google"، لذا نضيف مطابِقًا ثالثًا لذلك.

  • وستتم إضافة جميع المطابقات إلى القائمة نفسها التي نكررها.

  • لذا تصبح المطابقة الآن حلقة تكرار على جميع المطابقات في قائمتنا، حيث نستدعي المطابقة على كل واحدة للعثور على جميع المطابقات التي تطابق الطلب.

  • لإجراء ذلك، يمكننا استخدام mapNotNull لفلترة العناصر غير المتطابقة والحصول على قائمة بالعناصر المتطابقة الصالحة فقط.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
  • في معظم الحالات، من المحتمل أن يكون لديك تطابق واحد فقط غير فارغ في قائمتك، ولكن هناك حالات يمكن أن يكون فيها أكثر من تطابق صالح واحد.

  • على سبيل المثال، لنفترض أنّنا قدّمنا شاشة خاصة للجلسة الرئيسية تعيد استخدام نمط معرّف الموارد المنتظم (URI) نفسه للجلسة، ولكن بدلاً من العنصر النائب للمعرّف، أصبح لديها الآن مسار ثابت للجلسة الرئيسية.

  • في هذه الحالة، يمكن أن يتطابق الطلب نفسه مع كلا المطابِقَين: مفتاح الجلسة الذي يساوي فيه المعرّف keynote وNavKey keynote.

  • بما أنّ 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 أساسًا متينًا للعمل عليه.

  • تعمل جميع الأنواع الأساسية، مثل القيم المنطقية والأعداد الصحيحة والأعداد العشرية وحتى التعدادات، بشكل تلقائي، ما يؤدي إلى فشل المطابقة تلقائيًا وعرض قيمة فارغة إذا تعذّر تحليل قيمة الرابط ليكون من هذا النوع المحدّد.

  • على سبيل المثال، إذا كانت مَعلمة رقم تعريف الجلسة عددًا صحيحًا بدلاً من سلسلة، سيؤدي إدخال سلسلة لا يمكن تحليلها كعدد صحيح إلى تعذُّر المطابقة.

    // pattern = "http://ads.android.com/session/{id}"
    @Serializable data class Session(val id: Int) : NavKey
  • وبالمثل، إذا أردت الحصول على وسيطات اختيارية، مثل بيانات الإحصاءات المرفقة بعنوان URL للويب، يمكن أن تكون هذه السمات في مفتاحك لها قيم تلقائية.

    // pattern = "http://ads.android.com/session/{id}#referrer={referrer}
    @Serializable data class Session(
      val id: Int,
      val referrer: String? = null,
    ) : NavKey
  • ستلاحظون هنا أنّنا حاولنا جاهدين فصل بنية عنوان URL للويب، والذي قد يملكه فريق مختلف، عن الفئات الآمنة من حيث النوع أو الفئات التي تمثّل مطابقة الرابط لصفحة معيّنة في تطبيقكم.

  • لأنّني أعلم أنّ بعضكم يتعامل مع روابط عميقة سيئة للغاية.

    // "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 ضخم، ويقولون "لنضعه في كل عنوان URL في تطبيقك".

  • الآن، هناك مسار للمضي قدمًا، حتى عندما لا يمكنك تغيير عنوان URL للويب الذي تم منحه لك، ولكنك لا تزال بحاجة إلى استخراج بعض المعلومات من ذلك.

  • لذا، فإن الأسلوب الأول هو توسيع فئة DeepLinkSerializer للتحكم بدقة في كيفية إلغاء تسلسل هذه الوسيطة أو تحويلها من سلسلة إلى خاصية آمنة من حيث النوع.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
  • ‫DeepLinkSerializer هو برنامج تضمين مساعد حول Kotlin Serialization يسهّل تنفيذ فئة التسلسل، لأنّ إدخال إلغاء التسلسل في هذه الحالة يكون دائمًا سلسلة من عنوان URL للويب.

    public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
  • لتنفيذ DeepLinkSerializer، عليك كتابة ثلاث طرق.

  • أولاً، اختاروا اسمًا للسلسلة.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      ...
    }
  • لا يلزم أن يكون الاسم في مساحة اسم، ولكن يجب أن يكون فريدًا بما يكفي لكي يعرف Kotlin Serialization متى يستخدم أداة التسلسل.

  • بعد ذلك، عليك تنفيذ عملية إلغاء التسلسل، والتي يتم تمريرها إلى السلسلة من عنوان URL للويب، وهي مسؤولة عن إنشاء الكائن الذي يمنع أخطاء الكتابة.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      override fun deserialize(value: String): Bitmap {
        val imageBytes = Base64.decode(value, Base64.DEFAULT)
       // This better be a small image…
        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
      }
    
       ...
    
    }
  • أخيرًا، عليك تنفيذ عملية التسلسل، وهي الطريقة المعاكسة، ويتم استخدامها إذا تم استخدام فئة المفتاح نفسها في حالات مثل rememberNavBackStack.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
    
      ...
    
      override fun serialize(value: Bitmap): String {
        val byteArrayOutputStream = ByteArrayOutputStream()
        // Compress the bitmap into the output stream (e.g., JPEG or PNG)
        value.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
        val byteArray = byteArrayOutputStream.toByteArray()
        // Encode the byte array to a Base64 string
        return Base64.encodeToString(byteArray, Base64.DEFAULT)
      }
    }
  • الآن، ستطبّقون التسلسل المخصّص على السمة التي تحتوي على هذه الفئة الخاصة، وسيتولى UriDeepLinkMatcher الويب الباقي.

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

المعالجة المُسبقة لوسيطات عناوين URL باستخدام أدوات مطابقة مخصّصة

  • ولكن ماذا لو أردت الحصول على جزء صغير فقط من البيانات من سلسلة Base64 أو ملف JSON المرفق بعنوان URL للويب؟

  • في هذه الحالة، قد يكون من المنطقي معالجة بعض عناوين URL للويب مسبقًا قبل أن تصل إلى 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 قبل كل رابط عميق إلى أن تريد إنشاء رابط عميق لشاشة فرعية داخل شاشة إعدادات.

  • الآن، أنت بحاجة إلى طبقة إضافية في الأنشطة السابقة الاصطناعية لهذا الرابط العميق المحدّد.

  • وبطبيعة الحال، يؤدي ذلك إلى ربط أداة إنشاء الأنشطة السابقة بكل أداة مطابقة للروابط لصفحة معيّنة.

  • من ناحية أخرى، نعلم أيضًا أنّ كل جزء من الثانية يسبق عرض الشاشة الصحيحة يكلّفكم أموالاً حقيقية.

  • لذلك لم نرغب في احتساب أنشطة سابقة اصطناعية لكل نتيجة مباراة.

  • أردنا فقط الحصول على أفضل تطابق.

  • إذًا، إذا أردنا منطقًا مخصّصًا لكل مطابِق، ولكننا لا نريد إنشاء أنشطة سابقة اصطناعية بشكل استباقي لكل مطابقة، فماذا نفعل؟

  • لذا، بعد بضع محاولات غير ناجحة، توصّلنا إلى نهج يوازن بين كلا المتطلّبَين.

  • بغض النظر عن نوع أداة المطابقة، يمكنك تعزيزها من خلال استدعاء 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، وهي الأنشطة السابقة الكاملة التي تم إنشاؤها بواسطة دالة lambda‏ withBackStack.

  • سمح لنا نهج الطبقات هذا بفصل المنطق داخل أدوات المطابقة عن أي منطق مخصّص تريد استخدامه داخل دالة lambda في withBackStack.

جولة سريعة وملخّص ومراجع

  • هذه جلسة موجزة، لذا أوشك وقتنا على الانتهاء، ولكن يمكننا استعراض بعض الميزات الأخرى التي تتوفّر في Navigation Three والتي ستجدونها في وصفات Nav Three.

  • حسنًا

  • لذلك، يمكنك إضافة فلاتر إلى أي مطابِق لتقصير منطق المطابقة واستبعاد المطابِق بسرعة استنادًا إلى الإجراء أو نوع 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 كجزء من منطقها.

  • يتيح لك ذلك دمج أُطر عمل اختبارات A/B أو الإشارات الخارجية.

  • ولدينا أيضًا واجهات برمجة تطبيقات كاملة تمنع أخطاء الكتابة في ما يتعلّق بالبيانات الإضافية.

    val extras = requestExtras {
        put(SectionExtrasKey, "A")
    }
  • ويتم تضمين كل هذه المنطق تقريبًا في رمز KMP المشترك في Nav3، لذا يمكن مشاركة جميع أدوات المطابقة ومنطق الأنشطة السابقة على جميع الأنظمة الأساسية.

  • لذا، إذا أردت الاطّلاع على كيفية توافق Navigation 3 مع الروابط العميقة، يمكنك الاطّلاع على وصفات Nav3.

  • نعم، وقد دمجنا أيضًا واجهات برمجة التطبيقات للروابط العميقة في وصفات حقن التبعية، بالإضافة إلى تطبيقنا الرئيسي NowInAndroid.

  • شكرًا على حضور جلستنا الموجزة، ونتمنى لك تجربة تصفّح ممتعة.

  • شكرًا لك.

الرمز

مقتطفات الرمز البرمجي

مقتطف Kotlin في الدقيقة 04:41 في فيديو "تصميم واجهة برمجة التطبيقات للروابط لصفحات في التطبيق ومطابقة أنماط معرّفات الموارد المنتظمة (URI)"
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
مقتطف Kotlin في الدقيقة 04:53 في فيديو "تصميم واجهة برمجة التطبيقات للروابط لصفحات في التطبيق ومطابقة أنماط معرّف الموارد المنتظم"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
مقتطف Kotlin في الدقيقة 05:04 في فيديو "تصميم واجهة برمجة التطبيقات للروابط لصفحات في التطبيق ومطابقة أنماط معرّفات الموارد المنتظمة (URI)"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
مقتطف Kotlin في الدقيقة 05:12 في فيديو "تصميم واجهة برمجة التطبيقات للروابط لصفحات في التطبيق ومطابقة أنماط معرّفات الموارد المنتظمة (URI)"
// 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 عند الدقيقة 05:48 في فيديو "تصميم واجهة برمجة التطبيقات للروابط لصفحات في التطبيق ومطابقة أنماط معرّفات الموارد المنتظمة (URI)"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
مقتطف Kotlin في الدقيقة 06:12 في فيديو "تصميم واجهة برمجة التطبيقات للروابط لصفحات في التطبيق ومطابقة أنماط معرّفات الموارد المنتظمة (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 في الدقيقة 07:48 في فيديو "تصميم واجهة برمجة التطبيقات للروابط لصفحات في التطبيق ومطابقة أنماط معرّفات الموارد المنتظمة"
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 في الدقيقة 09:15 في "إلغاء التسلسل الآمن للنوع وأدوات التسلسل المخصّصة"
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
مقتطف Kotlin في الدقيقة 09:27 في "إلغاء التسلسل الآمن للنوع وأدوات التسلسل المخصّصة"
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
مقتطف Kotlin في الدقيقة 09:53 في "إلغاء التسلسل الآمن للنوع وأدوات التسلسل المخصّصة"
// "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 عند الدقيقة 10:51 في "إلغاء التسلسل الآمن للنوع وأدوات التسلسل المخصّصة"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
مقتطف Kotlin في الدقيقة 11:01 في "إلغاء التسلسل الآمن للنوع وأدوات التسلسل المخصّصة"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

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

   ...

}
مقتطف Kotlin في الدقيقة 11:13 في "إلغاء التسلسل الآمن للنوع وأدوات التسلسل المخصّصة"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {

  ...

  override fun serialize(value: Bitmap): String {
    val byteArrayOutputStream = ByteArrayOutputStream()
    // Compress the bitmap into the output stream (e.g., JPEG or PNG)
    value.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
    val byteArray = byteArrayOutputStream.toByteArray()
    // Encode the byte array to a Base64 string
    return Base64.encodeToString(byteArray, Base64.DEFAULT)
  }
}
مقتطف Kotlin في الدقيقة 11:24 في "إلغاء التسلسل الآمن للنوع وأدوات التسلسل المخصّصة"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}

@Serializable data class Session(
  val id: String,
  @Serializable(BitmapSerializer::class)
  val placeholderImage: Bitmap? = null
) : NavKey
مقتطف Kotlin في الدقيقة 12:09 في "المعالجة المُسبقة لوسيطات عناوين URL باستخدام أدوات مطابقة مخصّصة"
class SessionUriDeepLinkMatcher(
  uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {

  override fun matchArguments(
    pathArgs: Map<String, List<String>>,
    queryArgs: Map<String, List<String>>,
    fragmentArgs: Map<String, List<String>>
  ): UriMatchResult<Session>? {
    ...
}
مقتطف Kotlin في الدقيقة 12:21 في "المعالجة المُسبَقة لوسيطات عناوين URL باستخدام أدوات مطابقة مخصّصة"
class SessionUriDeepLinkMatcher(
  uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {

  override fun matchArguments(
    pathArgs: Map<String, List<String>>,
    queryArgs: Map<String, List<String>>,
    fragmentArgs: Map<String, List<String>>
  ): UriMatchResult<Session>? {
    val finalQueryArgs = buildMap {
      putAll(queryArgs)
      val jsonBlob = remove("session")?.firstOrNull()
      if (jsonBlob != null) { put("session", extractSessionIdFromJson(jsonBlob)) }
    }
    return super.matchArguments(pathArgs, finalQueryArgs, fragmentArgs)
}
مقتطف Kotlin في الدقيقة 14:31 في فيديو "إنشاء أنشطة سابقة اصطناعية للروابط لصفحات في التطبيق"
val homeMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
  listOf(Home, matchResult.key)
}

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

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

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

val backStack = bestMatch?.backStack ?: listOf(Home)
مقتطف Kotlin في الدقيقة 15:52 في "Lightning round, summary, and resources"
@Serializable object ImagesKey: NavKey

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

val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
مقتطف Kotlin في الدقيقة 16:24 في "Lightning round, summary, and resources"
val extras = requestExtras {
    put(SectionExtrasKey, "A")
}