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

الدروس المستفادة من تجربة WhatsApp مع Wear OS

سماعات

  • مارتن بيلانت

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

  • Subhag Bhatia

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

لمحة

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

يمكنك الاستماع مباشرةً إلى مهندسي WhatsApp حول كيفية ترقية تطبيق Wear OS على نطاق واسع. تتناول هذه الجلسة القرارات المتعلّقة بالبنية وتجربة المستخدم وراء دمج Material 3 واعتماد تطبيقات Wear المصغّرة.

يمكنك الاطّلاع مباشرةً على الدروس والأنماط والأدوات التي استخدمها الفريق لتحديث تطبيقه وتقديم تجربة مستخدم متّسقة.

ملخّص

  • تؤدي مشاركة رمز هاتف Android مع Wear OS إلى تقليل العمل المكرّر، ولكنها تتطلب حقن التبعية وعمليات إلغاء البيان. يجب أن تقارن الاختبارات بين مجموعات الربط المتعدّد حتى لا تختفي الميزات المُقسَّمة إلى وحدات بدون إشعار.
  • تمنع ميزة "إدارة ربط الإشعارات أثناء التشغيل" تكرار التنبيهات مع السماح للساعة بالعمل بشكل مستقل. يؤدي إيقاف ميزة "الربط" عند تسجيل الدخول وإعادة تفعيلها عند تسجيل الخروج إلى منع تكرار الاهتزاز في الهواتف التي تستخدم ميزة "النسخ المطابق".
  • تتطلّب عمليات نقل البيانات إلى Material 3 على الشاشات الدائرية إدارة التغييرات السلوكية إلى جانب التنسيق المرئي. يعني استخدام TransformingLazyColumn بشكل صحيح تطبيق عمليات التحويل على مستوى واحد فقط وتجنُّب تخزين المواضع مؤقتًا باستخدام remember في Compose.
  • يمكن أن تؤدي حدود انتهاء صلاحية التطبيق إلى انخفاض مفاجئ في عدد المستخدمين النشطين إذا لم يتم تحديث الساعات على الفور. توجّه تذكيرات التحديثات المُجدوَلة المستخدمين إلى Google Play قبل انتهاء صلاحية الإصدار الذي تبلغ مدته تسعين يومًا.

Gemini Notebook

المواضيع

  • wear-os
  • material-3
  • jetpack-compose
  • التقسيم إلى وحدات
  • الإشعارات
  • tiles-and-widgets

النص

تنزيل النص

مقدمة

  • مرحبًا،

  • اسمي "مارتن".

  • وأنا "سوبهاغ".

  • نحن مهندسون في Meta ضمن الفريق المسؤول عن تطبيق WhatsApp على Wear OS.

  • وقد حدّثنا تطبيقنا مؤخرًا.

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

  • ما يميّز تطبيق WhatsApp على Wear OS هو أنّه يعمل بدون هاتفك.

  • بعد ربط الجهاز، يعمل كجهاز WhatsApp مستقل، ويتم تشفيره بشكل تام بين الأطراف تمامًا مثل هاتفك، ويكون له قاعدة بياناته الخاصة واتصاله بالخادم.

  • وبالتالي، يستمر في العمل حتى على شبكة Wi-Fi أو LTE عندما يكون هاتفك خارج النطاق.

  • أطلقنا تطبيق WhatsApp لأول مرة على Wear OS في عام 2023.

  • وكانت تعمل من خلال برنامج صغير ومحمول مكتوب بلغة Rust.

  • لقد ساعدنا في طرح التطبيق بسرعة.

  • ومع ذلك، كان هذا تطبيقًا ثانيًا ومستقلاً من WhatsApp، مما يعني أنه كان لا بد من إنشاء كل بروتوكول وتشفير وميزة مرتين.

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

  • كما أن كونها محمولة جعل التعامل مع الخلفية أمرًا صعبًا.

  • تعامل العميل مع سلاسل المحادثات والمؤقتات والمقابس الخاصة به، وهي الأشياء التي يريد Android إدارتها نيابةً عنك.

  • على أجهزة Android، هناك قواعد لتشغيل التطبيقات في الخلفية.

  • عندما يكون الجهاز في وضع السكون، يتم قطع الاتصال بالشبكة.

  • بعد تخزين العملية مؤقتًا، يكون وقت التنفيذ محدودًا أو غير متاح.

  • أنت بحاجة إلى خدمة أو وظيفة أو دفعة للتغلب على ذلك.

  • لذلك كان على تطبيق الساعة الذكية ربطها يدويًا.

  • لكنّ قاعدة الرموز البرمجية في Android تنفّذ ذلك حاليًا.

  • لذلك، قررنا إعادة استخدام قاعدة رموز Android البرمجية والانتقال إليها، لأنّها خضعت لاختبارات مكثّفة على مليارات الأجهزة وتتّبع قواعد Android.

الانتقال إلى قاعدة رموز برمجية مشتركة

  • بهذه الطريقة، لا يلزم إنشاء ميزات جديدة مرتين.

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

  • ما يتبقّى هو مكتبة headless تستخدم Kotlin flows كواجهة برمجة تطبيقات.

  • ومع ذلك، تكمن المشكلة في أنّه لا يمكنك تفكيك تطبيق بهذا الحجم دفعة واحدة.

  • تجمع بعض الوحدات بين واجهة المستخدم والبنية الأساسية.

  • وتعتمد ميزات أخرى على ميزات لا تحتاج إليها في تطبيق Wear OS.

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

  • لنبدأ بالتقسيم إلى وحدات.

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

  • لذلك، نعتمد بشكل كبير على حقن التبعية، خاصةً الاختيارية والمتعددة.

  • نستخدم حقن التبعية الداخلي، ولكنه يتطابق بشكل وثيق مع ما يقدمه Dagger.

  • لذا، فإنّ الأمثلة مكتوبة بلغة Dagger.

  • باستخدام الربط الاختياري، في Dagger BindsOptionalOf، يمكنك الإعلان عن تبعية على أنها اختيارية، على سبيل المثال استدعاء الواجهة الخلفية.

    @Module
    interface CallingModule {
        @BindsOptionalOf
        fun callingBackend(): CallingBackend
    }
    
    class Calls @Inject constructor(
        val backend: Optional<CallingBackend>,
    ) {
        fun start(): CallResult =
            backend.getOrNull()?.start()
                ?: CallResult.NotAllowed
    }
  • بعد ذلك، إذا كان إصدار الساعة يتضمّن وحدة توفّر عملية التنفيذ، ستكون الخلفية التي تم إدخالها متوفّرة، وسيعمل الاتصال.

  • إذا حذفت الوحدة، ستكون فارغة، ويمكن أن تعرض واجهة برمجة التطبيقات NotAllowed.

  • يعمل الربط المتعدد بالطريقة نفسها، ولكن مع المجموعات.

    @Module
    interface CoreModule {
        @Multibinds
        fun handlers(): Set<MessageHandler>
    }
    
    @Module
    interface ReactionsModule {
        @Binds @IntoSet
        fun reactions(impl: ReactionHandler):
     MessageHandler
    }
    
    class MessageDispatcher @Inject constructor(
          private val handlers: Set<MessageHandler>,
    )
  • يمكنك تعريف مجموعة باستخدام multibinds، على سبيل المثال MessageHandler، وبعد ذلك يمكن لكل وحدة ميزات إضافة معالج خاص بها باستخدام into set multibinding.

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

  • بهذه الطريقة، يكون السؤال حول ما إذا كانت الميزة متوافقة مع الرسم البياني للإصدار.

  • ومع ذلك، فإنّ هذا الفصل يعمل في كلا الاتجاهين.

  • في حال استخدام المراجع المباشرة، سيؤدي حذف وحدة إلى تعطيل عملية الإنشاء.

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

  • وهذا ما حققناه بالضبط.

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

  • وأدّى ذلك إلى ظهور مدة الرسائل الصوتية على أنّها صفر ثانية.

  • فكيف يمكن ضمان عدم حدوث ذلك؟

  • حسنًا، أنت بحاجة إلى اختبار لإعادة التحقق.

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

  • بهذه الطريقة نضمن أن يكون لدينا دائمًا التنفيذ الصحيح.

  • يتناول المسار الثاني ما لم نتمكّن من إزالته بعد.

  • ستوفّر عليك إعادة استخدام الوحدات الكثير من العمل، ولكن سيتم دمج كل ما تعلنه في ملف البيان.

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

  • توفّر أداة دمج ملفات البيان سمة tools:node="remove" مفيدة تتيح لك إزالة المكوّنات.

    <!-- remove unused Main activity node -->
    <activity android:name="com.whatsapp.Main"
              tools:node="remove" />
    
    <!-- redirect any calls to a new Activity -->
    <activity-alias
         android:name="com.whatsapp.Main"
         android:targetActivity=".MyNewActivity" />
    
    <provider
    android:name="…MigrationContentProvider"
            tools:node="remove" />
  • وبمجرد خروجها، يمكن للمقلص إزالة الرمز أيضًا.

  • إذا كان هناك شيء لا يزال بحاجة إلى الوصول إلى النشاط، يمكنك استخدام activity-alias لإعادة التوجيه إلى فئة جديدة.

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

  • تعمل الموارد بالطريقة نفسها.

  • تمنحك ميزة "إعادة الاستخدام" سلاسل نصية للهاتف مجانًا.

  • ومع ذلك، فإن بعضها يعد بأشياء لا تستطيع الساعة القيام بها.

  • لنلقِ نظرة على مثال في النهاية على إشعار التشفير الذي يظهر في بداية كل محادثة.

  • على الهاتف، ينتهي الإشعار برابط "مزيد من المعلومات".

  • ومع ذلك، لا يوجد شيء وراء الرابط على الساعة.

  • لذا، ما نفعله هو إعادة تعريف المفتاح نفسه في وحدة ذات أولوية أعلى على الساعة واستبدال السلسلة بـ "استخدِم هاتفك لمعرفة المزيد".

    <!-- phone -->
    <string name="…_disclosure">… Learn more</string>
    
    <!-- wearable override, same key -->
    <string name="…_disclosure">…Use your phone to learn more.</string>
  • وبعد ذلك، يختار دمج الموارد المورد الخاص بنا.

  • تحتاج أيضًا إلى طريقة تضمن عدم استبدال السلسلة عن طريق الخطأ.

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

إدارة ميزة "مزامنة الإشعارات"

  • لنتحدّث الآن عن الإشعارات.

  • بشكل تلقائي، ينقل نظام Wear OS إشعارات هاتفك أو يعكسها.

  • هذا رائع، ولكن لا يمكنني تنفيذ ذلك إلا إذا كان هاتفك متصلاً.

  • ونريد أن يعمل WhatsApp بشكل مستقل، لذا ينشر تطبيقنا إشعاراتنا الخاصة.

  • ولكن إذا ظلّت ميزة "الربط" مفعَّلة، ستصدر كل رسالة صوتًا مرتين.

  • ويؤدي إيقاف ميزة "الربط" إلى حلّ مشكلة التكرار.

  • هناك علامة بيان ثابتة، ولكن يتم تطبيقها من التثبيت قبل تسجيل دخول المستخدم.

  • لذا، إذا ثبّت التطبيق ولم تسجّل الدخول، ستفقد فجأةً جميع الإشعارات، وهذا ليس أمرًا جيدًا، أليس كذلك؟

  • لذلك نضبطه في وقت التشغيل باستخدام BridgingManager.

    isRegistered.collect {
      when (it) {
             // watch posts its own
        true  -> bridging.setEnabled(false)
     // phone bridges again
        false -> bridging.setEnabled(true)
      }
    }
    
    fun setEnabled(bridgingEnabled){
       bridgingManager.setConfig(
    BridgingConfig.Builder(context,
    bridgingEnabled)
          .build())
    }
  • عند تسجيل الدخول، أوقفنا ميزة الربط.

  • وعند تسجيل الخروج، نعيد تفعيلها.

  • ومع ذلك، يُرجى العِلم أنّه لا تتوفّر واجهة برمجة تطبيقات لقراءة القيم مرة أخرى، وقد لاحظنا أنّ الساعة تفقدها.

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

  • الموضوع الأخير الذي أردت مناقشته هو، كيف يمكنك تسجيل الخروج؟

  • عند إلغاء ربط ساعة بحسابك، يتوقّع المستخدمون أن تتم إزالة الرسائل منها.

  • لذلك نحذف كل شيء - الرسائل والمفاتيح والإعدادات.

  • ولكن كيف يمكن إجراء ذلك بأمان؟

  • إذا حذفت الملفات بينما لا تزال سلاسل التعليمات في الخلفية تكتب فيها، فستظهر لك استثناءات SQLite، أو ما هو أسوأ من ذلك، ستتم الكتابة في قواعد البيانات التي تمت إعادة إنشائها حديثًا.

  • ولا يمكنك محو حالة عملية بدون إيقافها.

  • يوفّر لك Android واجهة برمجة التطبيقات clearApplicationUserData التي تمحو البيانات وتوقف التطبيق، ولكنّها تعيد أيضًا ضبط الأذونات.

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

  • لذلك نوقف العملية بأنفسنا.

    clearActivityBackStack()
    
    // enumerate our own, kill each
    killOtherProcesses()
    
    // no threads alive — now safe
    deleteDataDirectories()
    
    // show logged out confirmation
    firePostCleanupIntent()
  • ننشئ خدمة صغيرة في عملية ثانوية، لا تحتوي على حقن التبعية أو مدير العمل، لذا لا تبدأ بقية التطبيق.

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

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

  • هذا كل ما يتعلق بالجزء الأول، والآن سأترك الكلمة لـ "سوبهاغ" ليتحدث عن تحديث واجهة المستخدم.

تحديث واجهة مستخدم Wear OS

  • شكرًا، مارتن.

  • بعد توفير البنية الأساسية المشتركة، ركّزنا على جعل WhatsApp يبدو وكأنه في بيئته الطبيعية على معصمك.

  • كان الجزء الأول من هذه الرحلة هو الانتقال إلى Material 3، ولكن أولاً، لنتساءل عن السبب.

  • على الشاشة المستديرة التي يهمّ فيها كل بكسل، قدّم لنا Material 3 المكوّنات المناسبة وأساس التصميم لتحديث WhatsApp ليناسب المعصم.

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

  • كنا ننتقل من Material 2 إلى Material 3، ولم يكن من الممكن أن يحدث ذلك بين عشية وضحاها.

  • كان لا بد من أن يتواجد الرمز القديم والجديد جنبًا إلى جنب خلف مفتاح تبديل.

  • نتيجة لذلك، فإن الشاشات التي تطابقت بصريًا مع Figma، لا تزال تظهر بعض السلوكيات المعطلة.

  • على سبيل المثال، لم تكن بعض الإيماءات الأساسية تعمل، مثل التمرير باستخدام التاج والسحب لليمين للرجوع.

  • كل هذا يعني أنّ نقل نظام التصميم ليس مجرد نقل مرئي، بل هو أيضًا نقل للسلوك.

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

  • هذا هو TransformingLazyColumn.

  • تتحوّل الصفوف بحيث لا يقتطعها الإطار الدائري.

  • يمكن أن تجعل الشاشة الديناميكية الأمور صعبة.

  • تصل رسائل جديدة، وتتحرك الصفوف، وتظهر الفقاعات بكل الأشكال.

  • لنرَ ما يعنيه تحقيق ذلك.

  • إذا لاحظت على اليسار، فإن الفقاعات تتقلص بشكل كبير للغاية.

  • كل رسالة هي عبارة عن فقاعة محادثة داخل بطاقة.

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

  • ونتيجةً لذلك، تحصل الفقاعة الفرعية على ضعف مقدار التحويل المقصود.

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

  • وهذا أيضًا مبدأ أساسي.

  • لقد حرصنا على أن يكون لكل عنصر تحويل واحد.

  • وهناك مشكلة أخرى: الفقاعات صغيرة وباهتة في منتصف الشاشة حيث من المفترض أن تكون بالحجم الكامل.

  • حسنًا، الحجم هو دالة لموضعها على الشاشة.

  • في الإصدار الأوّلي، حفظنا حجم الملاءمة باستخدام دالة remember في Compose، ولكن انتهى الأمر أيضًا بحفظ موضعه القديم.

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

  • ويؤدي ذلك إلى حدوث هذا الخطأ.

  • لحلّ هذه المشكلة، توقّفنا عن حفظ الحجم باستخدام طلب remember وتخلّصنا من الطلب.

  • وهذا أيضًا درس مستفاد.

  • عندما يتحرك صف، يجب أن يتحرك تحويله معه.

  • يمكنك هنا الاطّلاع على الإصلاحَين في الرمز.

    // FIX #1: ONE OWNER
    CompositionLocalProvider(
      LocalSurfaceTransformation provides null
    ) {
      M3Card(transformation = t) {
        SharedChatEntry(...)
      }
    }
    
    // FIX #2: CURRENT ITEM SCOPE
    val t = SurfaceTransformation(spec)
    // BUG #1: DOUBLE MORPH
    M3Card(transformation = t) {
      CompositionLocalProvider(
        LocalSurfaceTransformation provides t
      ) { SharedChatEntry(...) }
    }
    
    // BUG #2: STALE ITEM SCOPE
    val t = remember(spec) {
      SurfaceTransformation(spec)
    }
  • في أعلى اليمين، يمكنك رؤية التحويل يتم تطبيقه على مستويين، وفي أعلى اليسار، يتم تطبيقه فقط على مستوى واحد، وهو بطاقة M3.

  • في أسفل يمين الشاشة، نستخدم استدعاء remember لتخزين التحويل مؤقتًا، ولكنه يخزّن أيضًا الموضع مؤقتًا.

  • وفي أسفل يسار الشاشة، نحسب التحويل مع مراعاة آخر موضع للفقاعة في كل مرة.

  • كما ترى، النتيجة هي تجربة متسقة عبر جميع الشاشات، حيث يظل المحتوى الرئيسي بالقرب من المركز، وتتحرك الصفوف بشكل طبيعي بالقرب من الحافة.

التعامل مع تنبيهات الإشعارات على المعصم

  • لنتحدّث الآن عن الإشعارات.

  • الآن تخيل أن معصمك يهتز، وتتحقق من ساعتك، وترى رسالة.

  • ولكن بعد لحظات، يهتز معصمك مرة أخرى، ومرة أخرى.

  • تتلقّى إشعارات متعدّدة، وعندما تتحقّق منها في كل مرة، يظهر لك المحتوى نفسه.

  • هذا مزعج حقًا.

  • لحلّ هذه المشكلة، طرحنا على أنفسنا سؤالَين.

  • ما هو الحدث الذي يجب أن ينبّه المستخدم؟

  • وأي حدث يجب أن يعدّل الإشعارات بدون صوت؟

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

  • الآن، عندما تصل رسالة جديدة، يهتز المعصم مرة واحدة بالضبط، بينما تقوم الأحداث غير الجديرة بالتنبيه بتحديث الإشعار بصمت.

  • وهذا أيضًا درس.

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

  • بالنسبة إلى الإشعارات:

  • في السابق، كانت إشعارات WhatsApp على Wear OS تتيح لك إجراء أمرَين.

  • يمكنك قراءة الرسالة أو فتح التطبيق.

  • إذا أردت الردّ على إشعار مباشرةً، لم يكن بإمكانك إجراء ذلك في السابق.

  • لذلك، سددنا هذه الفجوة باستخدام إجراء الإشعارات في Android مع واجهة برمجة التطبيقات RemoteInput.

  • يمكن للمستخدم الآن كتابة الردّ أو قوله أو النقر على ردّ سريع.

  • لكن تذكر أن الضغط على إرسال ليس النهاية.

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

  • إذا تعذّر تسليم رسالة، يمكنك إعادة المحاولة بنقرة واحدة.

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

نقل البيانات من المربّعات إلى التطبيقات المصغّرة

  • بعد ذلك، سنتحدّث عن المربّعات والتطبيقات المصغّرة.

  • كما ترون، شاشة المعلومات هي شاشة كاملة يمكنكم الوصول إليها من خلال التمرير سريعًا على خلفية شاشة الساعة.

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

  • تستند كل من مربّعات WhatsApp وعناصر واجهة المستخدم إلى البيانات الأساسية نفسها.

  • وهذا أيضًا أمر غير متوقّع.

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

  • نحن نصف فقط واجهة المستخدم والبيانات، ويقوم النظام برسمها.

  • بالإضافة إلى ذلك، لا يمكن تشغيل رمز تطبيقك من خلال النقر على أي من الرموز.

  • يمكنك فقط أن تطلب منه فتح شاشة.

  • وهذا هو درسنا.

  • إذا كان النظام يرسم واجهة المستخدم، فهذا جزء من المنتج ومقاييس الاختبار.

  • سيتعمّق "فرانك" و"يوري" في حديثهما عن التطبيقات المصغّرة.

منع الانخفاض الحاد في عدد المستخدمين النشطين من خلال تذكيرات التحديث

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

  • تخيَّل الآن أنّك مطوّر، وسجّلت الدخول إلى لوحات البيانات في أحد الأيام، ورأيت انخفاضًا كبيرًا في عدد المستخدمين النشطين.

  • حسنًا، في البداية، سيكون الأمر مقلقًا للغاية.

  • وبعد ذلك، قد تبدأ في تصحيح الأخطاء.

  • وقد تشعرون أنّ هناك عطلاً أو أنّ هناك إصدارًا سيئًا.

  • لكن بالنسبة إلينا، لم يكن أي من هذه الأمور صحيحًا.

  • تبيّن أنّ كل إصدار من تطبيق WhatsApp له صلاحية مدمجة تبلغ 90 يومًا.

  • لنفترض أنّ المستخدمين عالقون في إصدار قديم، بينما تستغرق الإصدارات الأحدث وقتًا للوصول إلى ساعاتهم.

  • نتيجةً لذلك، لا يغادر المستخدمون التطبيق، ولكن يتوقف التطبيق عن العمل.

  • لذلك جعلنا تحديث التطبيق جزءًا من المنتج.

  • كما ترون على الشاشة، قبل 15 يومًا من انتهاء صلاحية التطبيق، يظهر هذا على الشاشة.

  • ثم يظهر مرة أخرى عندما يتبقى 7 و5 و3 و2 ويوم واحد.

  • ويمكن للمستخدم اختيار رفضه أو الانتقال إلى "متجر Google Play" لتحديث التطبيق.

  • يساعد ذلك في تذكير المستخدمين عدة مرات باقتراب انتهاء صلاحية التطبيق، كما يمنحهم فرصًا متعددة لتحديثه.

  • وهذا هو الدرس الأخير.

  • لا تنتهي مهمتنا كمطوّرين عند طرح تحديث بنسبة %100.

  • ولا تكتمل إلا عندما يتلقّى المستخدمون تحديثاتنا فعليًا.

  • وهكذا نكون قد وصلنا إلى نهاية الفيديو.

  • إذا أردت مواصلة المحادثة، يُرجى الانضمام إلينا خلال ساعات العمل.

  • والآن، سيتحدّث "فرانك" و"يوري" عن التطبيقات المصغّرة في Wear.

  • شكرًا لك.

الرمز

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

مقتطف Kotlin في الدقيقة 02:27 في "الانتقال إلى قاعدة رموز برمجية مشتركة"
@Module
interface CallingModule {
    @BindsOptionalOf
    fun callingBackend(): CallingBackend
}

class Calls @Inject constructor(
    val backend: Optional<CallingBackend>,
) {
    fun start(): CallResult =
        backend.getOrNull()?.start()
            ?: CallResult.NotAllowed
}
مقتطف Kotlin في الدقيقة 02:47 في فيديو "الانتقال إلى قاعدة رموز برمجية مشتركة"
@Module
interface CoreModule {
    @Multibinds
    fun handlers(): Set<MessageHandler>
}

@Module
interface ReactionsModule {
    @Binds @IntoSet
    fun reactions(impl: ReactionHandler):
 MessageHandler
}

class MessageDispatcher @Inject constructor(
      private val handlers: Set<MessageHandler>,
)
مقتطف XML عند الطابع الزمني 04:16 في فيديو "الانتقال إلى قاعدة رموز برمجية مشتركة"
<!-- remove unused Main activity node -->
<activity android:name="com.whatsapp.Main"
          tools:node="remove" />

<!-- redirect any calls to a new Activity -->
<activity-alias
     android:name="com.whatsapp.Main"
     android:targetActivity=".MyNewActivity" />

<provider
android:name="…MigrationContentProvider"
        tools:node="remove" />
مقتطف XML في الدقيقة 05:03 في "الانتقال إلى قاعدة رموز برمجية مشتركة"
<!-- phone -->
<string name="…_disclosure">… Learn more</string>

<!-- wearable override, same key -->
<string name="…_disclosure">…Use your phone to learn more.</string>
مقتطف Kotlin في الدقيقة 05:59 في قسم "إدارة نقل الإشعارات"
isRegistered.collect {
  when (it) {
         // watch posts its own
    true  -> bridging.setEnabled(false)
 // phone bridges again
    false -> bridging.setEnabled(true)
  }
}

fun setEnabled(bridgingEnabled){
   bridgingManager.setConfig(
BridgingConfig.Builder(context,
bridgingEnabled)
      .build())
}
مقتطف Kotlin في الدقيقة 07:06 في "إدارة نقل الإشعارات"
clearActivityBackStack()

// enumerate our own, kill each
killOtherProcesses()

// no threads alive — now safe
deleteDataDirectories()

// show logged out confirmation
firePostCleanupIntent()
مقتطف Kotlin في الدقيقة 10:30 في فيديو "تحديث واجهة مستخدم Wear OS"
// FIX #1: ONE OWNER
CompositionLocalProvider(
  LocalSurfaceTransformation provides null
) {
  M3Card(transformation = t) {
    SharedChatEntry(...)
  }
}

// FIX #2: CURRENT ITEM SCOPE
val t = SurfaceTransformation(spec)
مقتطف Kotlin في الدقيقة 10:30 في فيديو "تحديث واجهة مستخدم Wear OS"
// BUG #1: DOUBLE MORPH
M3Card(transformation = t) {
  CompositionLocalProvider(
    LocalSurfaceTransformation provides t
  ) { SharedChatEntry(...) }
}

// BUG #2: STALE ITEM SCOPE
val t = remember(spec) {
  SurfaceTransformation(spec)
}