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

درس‌هایی از سفر Wear OS واتساپ

بلندگو

  • مارتین پلانت

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

  • Subhag Bhatia

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

درباره

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

مستقیماً از مهندسان «واتساپ» بشنوید که چگونه برنامه 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 در تیم پشتیبانی «واتساپ» در Wear OS هستیم.

  • و اخیراً برنامه‌مان را مدرن کرده‌ایم.

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

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

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

  • بنابراین، حتی وقتی تلفنتان خارج از محدوده باشد، این ویژگی همچنان ازطریق Wi-Fi یا LTE کار می‌کند.

  • ما برای اولین‌بار «واتساپ» را در سال ۲۰۲۳ در Wear OS راه‌اندازی کردیم.

  • این برنامه با یک کارخواه فشرده و قابل‌حمل که به زبان Rust نوشته شده بود کار می‌کرد.

  • این پلاتفرم به ما کمک کرد تا سریعاً به مرحله تولید برسیم.

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

  • بنابراین برنامه ساعت همیشه درحال به‌روزرسانی بود.

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

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

  • در Android، پس‌زمینه قوانینی دارد.

  • وقتی در حالت چرت زدن هستید، شبکه را ازدست می‌دهید.

  • وقتی فرایند شما در حافظه پنهان ذخیره می‌شود، زمان اجرای محدود دارید یا اصلاً زمان اجرا ندارید.

  • برای غلبه بر آن به سرویس، کار، یا ارسال مستقیم نیاز دارید.

  • بنابراین برنامه ساعت ما باید این کار را به‌صورت دستی انجام می‌داد.

  • اما پایگاه کد Android ما ازقبل این کار را انجام می‌دهد.

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

انتقال به پایگاه کد مشترک

  • به این ترتیب، ویژگی‌های جدید نباید دو بار ساخته شوند.

  • هدف ساده بود: منطق کسب‌وکار را حفظ کنیم و واسط کاربر را برداریم.

  • آنچه باقی می‌ماند یک کتابخانه بدون سر است که از جریان‌های Kotlin به‌عنوان میانای برنامه‌سازی کاربردی خود استفاده می‌کند.

  • اما چالش این است که نمی‌توانید برنامه‌ای با این اندازه را یک‌باره ازهم باز کنید.

  • برخی‌از واحدها واسط کاربر را با زیرساخت ترکیب می‌کنند.

  • برخی‌از آن‌ها به ویژگی‌هایی بستگی دارند که در برنامه Wear OS به آن‌ها نیاز ندارید.

  • بنابراین در دو مسیر کار کردیم: به مدولارسازی ادامه دادیم و آنچه را که هنوز نمی‌توانستیم حذف کنیم، حذف یا بازنویسی کردیم.

  • بیایید با مدولارسازی شروع کنیم.

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

  • بنابراین ما به‌شدت به DI، به‌ویژه اختیاری و چنداتصالی، متکی هستیم.

  • ما از DI داخلی استفاده می‌کنیم، اما این DI بسیار شبیه به چیزی است که 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 را برگرداند.

  • Multibinding برای همین کار می‌کند، اما برای مجموعه‌ها.

    @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 اعلان‌های تلفنتان را پل‌بندی یا منعکس می‌کند.

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

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

  • اما اگر پل‌سازی روشن بماند، هر پیام دو بار زنگ می‌زند.

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

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

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

  • بنابراین آن را در زمان اجرا بااستفاده از 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())
    }
  • وقتی به سیستم وارد شدید، پل‌سازی را خاموش کردیم.

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

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

  • بنابراین کاری که انجام می‌دهیم این است که هرگاه ساعت از شارژر جدا می‌شود یا برنامه به‌روز می‌شود، آن را دوباره می‌نویسیم.

  • آخرین موضوعی که می‌خواستم درباره آن صحبت کنم این است که چگونه از سیستم خارج می‌شوید؟

  • وقتی پیوند ساعت را از حسابتان قطع می‌کنید، افراد انتظار دارند پیام‌ها هم با آن برود.

  • بنابراین همه چیز را حذف می‌کنیم-- پیام‌ها، کلیدها، تنظیمات.

  • اما چگونه این کار را به‌طور ایمن انجام دهیم؟

  • حذف فایل‌ها درحالی‌که رشته‌های پس‌زمینه همچنان در آن می‌نویسند، استثناهای SQLite دریافت می‌کنید، یا حتی بدتر، در پایگاه‌های داده جدیداً بازسازی‌شده می‌نویسد.

  • و واقعاً نمی‌توانید وضعیت فرایند را بدون متوقف کردن آن پاک کنید.

  • Android به شما clearApplicationUserData API را ارائه می‌دهد که داده‌ها را پاک می‌کند و برنامه را متوقف می‌کند، اما اجازه‌ها را نیز بازنشانی می‌کند.

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

  • بنابراین خودمان فرایند را متوقف می‌کنیم.

    clearActivityBackStack()
    
    // enumerate our own, kill each
    killOtherProcesses()
    
    // no threads alive — now safe
    deleteDataDirectories()
    
    // show logged out confirmation
    firePostCleanupIntent()
  • ما سرویس کوچکی در فرایند ثانویه می‌سازیم که فاقد DI و مدیر کار است، بنابراین بقیه برنامه را راه‌اندازی نمی‌کند.

  • پس‌از خروج از سیستم، این سرویس همه فرایندهای دیگر را در برنامه ما متوقف می‌کند و تنها زمانی همه داده‌ها را حذف می‌کند که هیچ‌چیز با آن رقابت نکند.

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

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

مدرن‌سازی واسط کاربر Wear OS

  • ممنون، مارتین.

  • با وجود زیرساخت مشترک، تمرکز ما بر این بود که WhatsApp را واقعاً در مچ دست شما قرار دهیم.

  • بخش اول این سفر، انتقال به Material 3 بود، اما ابتدا بیایید بپرسیم چرا.

  • در نمایشگر گرد که هر پیکسل اهمیت دارد، Material 3 مؤلفه‌های مناسب و پایه طراحی را برای مدرن‌سازی WhatsApp برای مچ دست به ما ارائه داد.

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

  • خب، ما درحال انتقال از Material 2 به 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 برای ذخیره کردن تبدیل در حافظه نهان استفاده می‌کنیم، اما این فراخوانی موقعیت را هم در حافظه نهان ذخیره می‌کند.

  • و در پایین سمت چپ، ما تبدیل را با درنظر گرفتن آخرین موقعیت حباب در هر بار محاسبه می‌کنیم.

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

مدیریت هشدارهای اعلان مچ دست

  • در ادامه، بیایید درباره اعلان‌ها صحبت کنیم.

  • حالا تصور کنید مچ دستتان می‌لرزد، ساعتتان را نگاه می‌کنید، و پیامی می‌بینید.

  • اما چند لحظه بعد، مچ دستتان دوباره و دوباره می‌لرزد.

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

  • واقعاً آزاردهنده است.

  • پس برای حل این مشکل، دو سؤال از خودمان پرسیدیم.

  • چه رویدادی باید به اعلان هشدار دهد که به کاربر هشدار دهد؟

  • و کدام رویداد باید اعلان‌ها را بی‌صدا به‌روز کند؟

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

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

  • و این هم یک درس است.

  • وقتی اعلان یکی از مسیرهای اصلی کاربر شما است، باید مطمئن شویم که هر هشدار واقعاً شایسته توجه کاربر است.

  • درباره موضوع اعلان‌ها.

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

  • می‌توانید پیام را بخوانید یا برنامه را باز کنید.

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

  • بنابراین بااستفاده از کنش اعلان Android با RemoteInput API این شکاف را پر کردیم.

  • اکنون کاربر می‌تواند آن را تایپ کند، بگوید، یا روی پاسخ سریع تک‌ضرب بزند.

  • اما به‌خاطر داشته باشید که زدن دکمه ارسال پایان کار نیست.

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

  • اگر پیامی ارسال نشود، گزینه امتحان مجدد با یک ضربه را ارائه می‌دهیم.

  • و این هم یک یادگیری کلیدی در حین توسعه این است که یک اقدام اعلان باید به‌عنوان یک گردش کار کامل سرتاسر درنظر گرفته شود.

انتقال از کاشی به ابزاره

  • بعدی، کاشی‌ها و ابزاره‌ها.

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

  • ازسوی‌دیگر، ابزاره‌ها چندین کارت کوچک‌تر هستند که چندتای آن‌ها می‌تواند در یک صفحه پیمایش‌شدنی جا شود.

  • «کاشی‌ها» و «ابزاره‌های» واتساپ از داده‌های زیربنایی یکسانی استفاده می‌کنند.

  • و این هم یک چرخش است.

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

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

  • همچنین، با تک‌ضرب زدن روی هرکدام از نمادها، کد برنامه‌تان اجرا نمی‌شود.

  • فقط می‌توانید از آن بخواهید صفحه‌ای را باز کند.

  • و این هم از درس ما.

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

  • فرانک و یوری در صحبت‌هایشان درباره ابزاره‌ها عمیق‌تر خواهند شد.

جلوگیری از ریزش کاربران فعال با یادآوری‌های به‌روزرسانی

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

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

  • خب، اول از همه، این موضوع بسیار نگران‌کننده خواهد بود.

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

  • و ممکن است احساس کنید که خرابی رخ داده است یا نسخه بدی درحال انتشار است.

  • ولی برای ما، هیچ‌کدام از این‌ها نبود.

  • مشخص شد که هر نسخه از برنامه «واتساپ» دارای تاریخ انقضای ۹۰ روزه داخلی است.

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

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

  • بنابراین به‌روزرسانی برنامه را بخشی از محصول قرار دادیم.

  • همان‌طور که در صفحه می‌بینید، ۱۵ روز قبل‌از انقضای برنامه، این پیام در صفحه ظاهر می‌شود.

  • سپس وقتی ۷، ۵، ۳، ۲، و ۱ روز باقی مانده باشد دوباره نشان داده می‌شود.

  • کاربر می‌تواند آن را ببندد یا برای به‌روزرسانی برنامه به «فروشگاه Play» برود.

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

  • و این آخرین یادگیری ما است.

  • کار ما به‌عنوان توسعه‌دهنده وقتی به‌روزرسانی را به ۱۰۰٪ می‌رسانیم تمام نمی‌شود.

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

  • به پایان رسیدیم.

  • اگر مایلید مکالمه را ادامه دهید، لطفاً در ساعات اداری با ما تماس بگیرید.

  • و نفرات بعدی فرانک و یوری هستند که درباره ابزاره‌های Wear صحبت می‌کنند.

  • متشکریم.

کد

تکه‌کدها

قطعه Kotlin در ۰۲:۲۷ در «انتقال به پایگاه کد مشترک»
@Module
interface CallingModule {
    @BindsOptionalOf
    fun callingBackend(): CallingBackend
}

class Calls @Inject constructor(
    val backend: Optional<CallingBackend>,
) {
    fun start(): CallResult =
        backend.getOrNull()?.start()
            ?: CallResult.NotAllowed
}
قطعه Kotlin در ۰۲:۴۷ در «انتقال به پایگاه کد مشترک»
@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 در ۰۴:۱۶ در «انتقال به پایگاه کد مشترک»
<!-- 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 در ۰۵:۰۳ در «انتقال به پایگاه کد مشترک»
<!-- phone -->
<string name="…_disclosure">… Learn more</string>

<!-- wearable override, same key -->
<string name="…_disclosure">…Use your phone to learn more.</string>
قطعه Kotlin در ۰۵:۵۹ در «مدیریت پل‌زنی اعلان»
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 در ۰۷:۰۶ در «مدیریت پل‌زنی اعلان»
clearActivityBackStack()

// enumerate our own, kill each
killOtherProcesses()

// no threads alive — now safe
deleteDataDirectories()

// show logged out confirmation
firePostCleanupIntent()
قطعه Kotlin در ۱۰:۳۰ در «مدرن‌سازی واسط کاربر Wear OS»
// FIX #1: ONE OWNER
CompositionLocalProvider(
  LocalSurfaceTransformation provides null
) {
  M3Card(transformation = t) {
    SharedChatEntry(...)
  }
}

// FIX #2: CURRENT ITEM SCOPE
val t = SurfaceTransformation(spec)
قطعه Kotlin در دقیقه ۱۰:۳۰ در «مدرن‌سازی واسط کاربر 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)
}