Apps foundation

บทเรียนจากเส้นทางการพัฒนาแอป Wear OS ของ WhatsApp

ลำโพง

  • Martin Pelant

    วิศวกรซอฟต์แวร์, Meta

  • Subhag Bhatia

    วิศวกรซอฟต์แวร์, Meta

เกี่ยวกับ

เกี่ยวกับการเสวนา

ฟังวิศวกรของ WhatsApp อธิบายโดยตรงเกี่ยวกับวิธีอัปเกรดแอป Wear OS ในวงกว้าง เซสชันนี้ครอบคลุมการตัดสินใจด้านสถาปัตยกรรมและ UX ที่อยู่เบื้องหลังการผสานรวม Material 3 และการนำวิดเจ็ต Wear มาใช้

รับข้อมูลเชิงลึกโดยตรงเกี่ยวกับบทเรียน รูปแบบ และเครื่องมือที่ทีมใช้เพื่ออัปเดตแอปและมอบประสบการณ์ของผู้ใช้ที่สอดคล้องกัน

สรุป

  • การแชร์โค้ดของโทรศัพท์ Android กับ Wear OS ช่วยลดการทำงานซ้ำซ้อน แต่ต้องใช้ Dependency Injection และการลบล้างไฟล์ Manifest การทดสอบควรเปรียบเทียบชุดการเชื่อมโยงหลายรายการเพื่อให้ฟีเจอร์ที่แยกเป็นโมดูลไม่หายไปโดยไม่ทราบสาเหตุ
  • การจัดการการเชื่อมต่อการแจ้งเตือนระหว่างรันไทม์จะป้องกันการแจ้งเตือนซ้ำซ้อน ขณะเดียวกันก็ช่วยให้นาฬิกาทำงานได้อย่างอิสระ การปิดการเชื่อมต่อเมื่อเข้าสู่ระบบและเปิดอีกครั้งเมื่อออกจากระบบจะหยุดการสั่นซ้ำของโทรศัพท์ที่มิเรอร์
  • การย้ายข้อมูล Material 3 บนจอแสดงผลแบบกลมต้องมีการจัดการการเปลี่ยนแปลงด้านลักษณะการทำงานควบคู่ไปกับการจัดรูปแบบภาพ การใช้ TransformingLazyColumn อย่างเหมาะสมหมายถึงการใช้การเปลี่ยนรูปแบบที่ระดับเดียวเท่านั้น และหลีกเลี่ยงการแคชตำแหน่งด้วย remember ของ Compose
  • ขีดจำกัดการหมดอายุของแอปอาจทำให้จำนวนผู้ใช้ที่ใช้งานอยู่ลดลงอย่างกะทันหันหากนาฬิกาอัปเดตไม่ทันเวลา การช่วยเตือนการอัปเดตตามกำหนดเวลาจะนำผู้ใช้ไปยัง Google Play ก่อนที่บิลด์ 90 วันจะหมดอายุ

Gemini Notebook

หัวข้อ

  • wear-os
  • material-3
  • jetpack-compose
  • การแยกเป็นโมดูล
  • การแจ้งเตือน
  • tiles-and-widgets

ข้อความถอดเสียง

บทนำ

  • สวัสดีทุกคน

  • ฉันชื่อ Martin

  • และฉัน Subhag

  • เราเป็นวิศวกรที่ Meta ในทีมที่อยู่เบื้องหลัง WhatsApp บน Wear OS

  • และเราเพิ่งปรับปรุงแอปให้ทันสมัยขึ้น

  • ดังนั้นวันนี้เราจะแชร์บทเรียนที่ได้จากการย้ายข้อมูลไปยังฐานของโค้ดที่ใช้ร่วมกัน แล้วสร้างประสบการณ์ใหม่ๆ บนฐานของโค้ดนั้น

  • สิ่งที่ทำให้ WhatsApp บน Wear OS น่าสนใจคือสามารถทำงานได้โดยไม่ต้องใช้โทรศัพท์

  • เมื่อลิงก์แล้ว อุปกรณ์ดังกล่าวจะทำหน้าที่เป็นอุปกรณ์ WhatsApp ของตัวเอง โดยมีการเข้ารหัสจากต้นทางถึงปลายทางเช่นเดียวกับโทรศัพท์ของคุณ พร้อมด้วยฐานข้อมูลและการเชื่อมต่อกับเซิร์ฟเวอร์ของตัวเอง

  • จึงทำงานได้แม้จะใช้ Wi-Fi หรือ LTE เมื่อโทรศัพท์อยู่นอกระยะสัญญาณ

  • เราเปิดตัว WhatsApp บน Wear OS เป็นครั้งแรกในปี 2023

  • ซึ่งขับเคลื่อนโดยไคลเอ็นต์แบบพกพาขนาดกะทัดรัดที่เขียนด้วย Rust

  • ซึ่งช่วยให้เราเข้าสู่เวอร์ชันที่ใช้งานจริงได้อย่างรวดเร็ว

  • อย่างไรก็ตาม การใช้งาน WhatsApp ครั้งที่ 2 นี้เป็นแบบอิสระ ซึ่งหมายความว่าต้องสร้างโปรโตคอล การเข้ารหัส และฟีเจอร์ทั้งหมด 2 ครั้ง

  • แอปสมาร์ทวอทช์จึงต้องคอยอัปเดตตามอยู่เสมอ

  • การเป็นอุปกรณ์พกพายังทำให้การจัดการพื้นหลังเป็นเรื่องยากด้วย

  • ไคลเอ็นต์จัดการชุดข้อความ ตัวจับเวลา และซ็อกเก็ตของตนเอง ซึ่งเป็นสิ่งที่ Android ต้องการจัดการให้คุณ

  • ใน Android การทำงานในเบื้องหลังมีกฎ

  • เมื่ออยู่ในโหมด Doze คุณจะสูญเสียการเชื่อมต่อเครือข่าย

  • เมื่อแคชกระบวนการแล้ว คุณจะมีเวลาดำเนินการที่จำกัดหรือไม่มีเลย

  • คุณต้องมีบริการ งาน หรือการพุชเพื่อเอาชนะปัญหานั้น

  • ดังนั้นแอปสมาร์ทวอทช์ของเราจึงต้องเชื่อมต่อข้อมูลเหล่านั้นด้วยตนเอง

  • แต่ฐานของโค้ด Android ของเราทำเช่นนั้นอยู่แล้ว

  • เราจึงตัดสินใจนำฐานของโค้ด Android มาใช้ซ้ำและย้ายข้อมูลไปไว้ที่นั่น ซึ่งผ่านการทดสอบอย่างหนักหน่วงบนอุปกรณ์หลายพันล้านเครื่องและเป็นไปตามกฎของ Android อยู่แล้ว

การย้ายไปยังฐานของโค้ดที่ใช้ร่วมกัน

  • วิธีนี้ช่วยให้ไม่ต้องสร้างฟีเจอร์ใหม่ 2 ครั้ง

  • เป้าหมายนั้นเรียบง่าย นั่นคือคงตรรกะทางธุรกิจไว้และนำ UI ออก

  • สิ่งที่เหลืออยู่คือไลบรารีแบบ Headless ซึ่งใช้ Kotlin Flow เป็น API

  • อย่างไรก็ตาม ความท้าทายคือคุณไม่สามารถแยกแอปขนาดนี้ได้ในครั้งเดียว

  • บางโมดูลจะผสม UI กับโครงสร้างพื้นฐาน

  • แอปอื่นๆ อาศัยฟีเจอร์ที่คุณไม่จำเป็นต้องใช้ในแอป Wear OS

  • เราจึงทำงานใน 2 แนวทาง ได้แก่ เรายังคงแยกเป็นโมดูลต่อไป และเราตัดหรือเขียนทับสิ่งที่ยังนำออกไม่ได้

  • มาเริ่มกันที่การแยกโมดูล

  • หากต้องการเก็บโค้ดไว้นอกบิลด์ คำสั่ง "หากนี่คือแอปโทรศัพท์" แบบง่ายๆ จะใช้ไม่ได้เนื่องจากไม่ได้นำโค้ดออก

  • เราจึงพึ่งพา DI เป็นอย่างมาก โดยเฉพาะอย่างยิ่ง Optional และ Multibinding

  • เราใช้ DI ภายใน แต่ก็เชื่อมโยงกับสิ่งที่ Dagger มีให้อย่างใกล้ชิด

  • ตัวอย่างจึงอยู่ใน Dagger

  • เมื่อใช้การเชื่อมโยงที่ไม่บังคับ ซึ่งใน Dagger คือ BindsOptionalOf คุณสามารถประกาศทรัพยากร Dependency เป็นแบบไม่บังคับได้ เช่น การเรียกใช้แบ็กเอนด์

    @Module
    interface CallingModule {
        @BindsOptionalOf
        fun callingBackend(): CallingBackend
    }
    
    class Calls @Inject constructor(
        val backend: Optional<CallingBackend>,
    ) {
        fun start(): CallResult =
            backend.getOrNull()?.start()
                ?: CallResult.NotAllowed
    }
  • จากนั้นหากบิลด์ของนาฬิกามีโมดูลที่ให้การติดตั้งใช้งาน แบ็กเอนด์ที่แทรกไว้จะปรากฏขึ้นและการโทรจะทำงาน

  • หากคุณละเว้นโมดูล โมดูลจะเป็นค่า Null และ API จะแสดงผล 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 จากนั้นโมดูลฟีเจอร์แต่ละโมดูลจะเพิ่มเครื่องจัดการของตนเองได้โดยใช้ multibinding ในชุด

  • หากไม่ต้องการรองรับการชำระเงิน คุณสามารถละเว้นโมดูลการชำระเงินทั้งหมดออกจากบิลด์ และชุดที่แทรกจะไม่รวมแฮนเดิลเลอร์นั้น

  • ด้วยวิธีนี้ ไม่ว่าฟีเจอร์จะรองรับหรือไม่ก็ตาม ก็จะเป็นคำถามสำหรับกราฟบิลด์

  • อย่างไรก็ตาม การแยกส่วนนี้มีทั้งข้อดีและข้อเสีย

  • เมื่อใช้การอ้างอิงโดยตรง การลบโมดูลจะทำให้บิลด์เสียหาย

  • เมื่อใช้การเชื่อมโยงแบบไม่บังคับหรือแบบหลายรายการ จะไม่มีการอ้างอิงโมดูล ดังนั้นการบิลด์จึงผ่าน และฟีเจอร์จะหายไปอย่างเงียบๆ หรือค่าเริ่มต้นจะทำงานแทน

  • และเราก็พบปัญหานี้จริงๆ

  • การเปลี่ยนโครงสร้างภายในโค้ดย้ายฟีเจอร์ไปยังโมดูลของตัวเอง และแอปบิลด์และแอปสมาร์ทวอทช์ของเราไม่ได้รวมฟีเจอร์ดังกล่าวไว้

  • ซึ่งส่งผลให้ข้อความเสียงแสดงระยะเวลาเป็น 0 วินาที

  • แล้วจะแน่ใจได้อย่างไรว่าเหตุการณ์นี้จะไม่เกิดขึ้น

  • คุณต้องทำการทดสอบเพื่อใส่เครื่องหมายถูกกลับเข้าไป

  • ในกรณีของเรา สิ่งที่เราทำคือเปรียบเทียบชุดการเชื่อมโยงหลายรายการระหว่างแอปโทรศัพท์กับแอปสมาร์ทวอทช์สำหรับอินเทอร์เฟซที่สำคัญ

  • วิธีนี้ช่วยให้เรามั่นใจได้ว่าเรามีการติดตั้งใช้งานที่ถูกต้องอยู่เสมอ

  • ตอนนี้แทร็กที่ 2 จะครอบคลุมสิ่งที่เรายังนำออกไม่ได้

  • การใช้โมดูลซ้ำจะช่วยประหยัดเวลาในการทำงาน แต่ทุกอย่างที่โมดูลประกาศจะผสานรวมเข้ากับไฟล์ Manifest ของคุณ

  • กิจกรรม Launcher ของโทรศัพท์จะเพิ่มไอคอนแอปสำรอง หรือ Content Provider ที่ไม่จำเป็นจะเริ่มทำงานพร้อมกับแอปของคุณ

  • เครื่องมือผสานไฟล์ Manifest มีแอตทริบิวต์ 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 เพื่อเปลี่ยนเส้นทางไปยังคลาสใหม่ได้

  • อย่างไรก็ตาม สิ่งที่ควรระวังคือแอตทริบิวต์เหล่านั้นจะใช้กับไฟล์ Manifest ที่มีลำดับความสำคัญต่ำกว่าเท่านั้น ดังนั้นลำดับการผสานจะกำหนดว่าระบบจะนำบางสิ่งออกหรือไม่

  • ทรัพยากรก็ทำงานในลักษณะเดียวกัน

  • การนำสตริงมาใช้ซ้ำจะช่วยให้คุณได้รับสตริงโทรศัพท์โดยไม่มีค่าใช้จ่าย

  • อย่างไรก็ตาม บางแอปก็สัญญาว่าจะทำในสิ่งที่นาฬิกาทำไม่ได้

  • ลองดูตัวอย่างที่ส่วนท้ายของประกาศเกี่ยวกับการเข้ารหัสซึ่งอยู่ที่จุดเริ่มต้นของการแชททุกครั้ง

  • ในโทรศัพท์ ข้อความจะลงท้ายด้วยลิงก์ "ดูข้อมูลเพิ่มเติม"

  • อย่างไรก็ตาม ไม่มีอะไรอยู่เบื้องหลังลิงก์ในนาฬิกา

  • ดังนั้นสิ่งที่เราทำคือประกาศคีย์เดียวกันอีกครั้งในโมดูลที่มีลำดับความสำคัญสูงกว่าในนาฬิกา และแทนที่สตริงด้วย "ใช้โทรศัพท์เพื่อดูข้อมูลเพิ่มเติม"

    <!-- phone -->
    <string name="…_disclosure">… Learn more</string>
    
    <!-- wearable override, same key -->
    <string name="…_disclosure">…Use your phone to learn more.</string>
  • จากนั้นเครื่องมือผสานทรัพยากรจะเลือกทรัพยากรของเรา

  • นอกจากนี้ คุณยังต้องมีวิธีตรวจสอบว่าคุณไม่ได้ลบล้างสตริงโดยไม่ตั้งใจ

  • ดังนั้นเราจึงสร้างกฎการสร้างที่ช่วยให้มั่นใจและจะล้มเหลวหากการลบล้างไม่อยู่ในรายการที่อนุญาต

การจัดการการเชื่อมต่อการแจ้งเตือน

  • ทีนี้เรามาพูดถึงการแจ้งเตือนกันบ้าง

  • โดยค่าเริ่มต้น Wear OS จะเชื่อมต่อหรือมิเรอร์การแจ้งเตือนในโทรศัพท์

  • ดีเลย แต่จะใช้ได้ก็ต่อเมื่อโทรศัพท์เชื่อมต่ออยู่เท่านั้น

  • และเราต้องการให้ WhatsApp ทำงานอย่างเป็นอิสระ ดังนั้นแอปของเราจึงโพสต์การแจ้งเตือนของเราเอง

  • แต่หากเปิดการเชื่อมต่อไว้ ข้อความทุกข้อความจะส่งเสียงเตือน 2 ครั้ง

  • การปิดการเชื่อมต่อจะช่วยแก้ไขปัญหาการซ้ำกันได้

  • มีแฟล็ก Manifest แบบคงที่ แต่จะมีผลตั้งแต่การติดตั้งก่อนที่ผู้ใช้จะเข้าสู่ระบบ

  • ดังนั้นหากคุณติดตั้งและไม่ได้เข้าสู่ระบบ คุณจะสูญเสียการแจ้งเตือนทั้งหมดทันที ซึ่งไม่ดีเลยใช่ไหม

  • ดังนั้นเราจึงตั้งค่าที่รันไทม์โดยใช้ 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 ซึ่งจะล้างข้อมูลและหยุดแอป แต่จะรีเซ็ตสิทธิ์ด้วย

  • และใน OEM บางราย การเชื่อมต่อการแจ้งเตือนจะเชื่อมโยงกับสิทธิ์การแจ้งเตือน ดังนั้นหากคุณใช้สิทธิ์นี้ คุณจะสูญเสียการมิเรอร์โทรศัพท์ไปด้วยในทันที

  • เราจึงหยุดกระบวนการนี้ด้วยตนเอง

    clearActivityBackStack()
    
    // enumerate our own, kill each
    killOtherProcesses()
    
    // no threads alive — now safe
    deleteDataDirectories()
    
    // show logged out confirmation
    firePostCleanupIntent()
  • เราสร้างบริการขนาดเล็กในกระบวนการรองซึ่งไม่มี DI และไม่มี Work Manager จึงไม่เริ่มแอปส่วนที่เหลือ

  • หลังจากที่คุณออกจากระบบ บริการนี้จะหยุดกระบวนการอื่นๆ ทั้งหมดในแอปของเรา และจะลบข้อมูลทั้งหมดเมื่อไม่มีกระบวนการใดๆ ทำงานอยู่เท่านั้น

  • เนื่องจากเราไม่ได้ใช้ clearApplicationUserData สิทธิ์จึงยังคงอยู่และเราสามารถเปิดการเชื่อมต่อการแจ้งเตือนได้ เพื่อให้การแจ้งเตือนทางโทรศัพท์กลับมา

  • นั่นคือส่วนที่ 1 และตอนนี้ฉันจะส่งต่อให้ Subhag พูดถึงการปรับปรุง UI ของเรา

การปรับปรุง UI ของ Wear OS ให้ทันสมัย

  • ขอบคุณ Martin

  • เมื่อมีโครงสร้างพื้นฐานที่ใช้ร่วมกันแล้ว เราจึงมุ่งเน้นที่การทำให้ WhatsApp ทำงานบนข้อมือของคุณได้อย่างราบรื่น

  • ส่วนแรกของการเดินทางนั้นคือการเปลี่ยนไปใช้ Material 3 แต่ก่อนอื่น เรามาถามกันก่อนว่าทำไม

  • บนจอแสดงผลแบบกลมที่ทุกพิกเซลมีความสำคัญ Material 3 มีคอมโพเนนต์และรากฐานการออกแบบที่เหมาะสมเพื่อปรับโฉม WhatsApp ให้ทันสมัยสำหรับข้อมือ

  • หากคุณสังเกตเห็นภาพที่ปรากฏบนหน้าจอ ก็อาจดูเหมือนเป็นการอัปเดต UI เท่านั้น

  • เรากำลังเปลี่ยนจาก Material 2 เป็น 3 ซึ่งไม่สามารถทำได้ในชั่วข้ามคืน

  • โค้ดเก่าและโค้ดใหม่ต้องอยู่ร่วมกันเบื้องหลังสวิตช์

  • ด้วยเหตุนี้ หน้าจอที่ตรงกับ Figma ในเชิงภาพจึงยังคงแสดงลักษณะการทำงานที่ผิดปกติบางอย่าง

  • ตัวอย่างเช่น การเลื่อนเม็ดมะยม การปัดไปทางขวาเพื่อย้อนกลับ ท่าทางสัมผัสพื้นฐานบางอย่างใช้งานไม่ได้

  • สรุปก็คือ การย้ายข้อมูลระบบการออกแบบไม่ได้เป็นเพียงการย้ายข้อมูลภาพเท่านั้น แต่ยังเป็นการย้ายข้อมูลพฤติกรรมด้วย

  • เมื่อพูดถึงพฤติกรรม หากคุณสังเกตเห็นแถวที่อยู่ใกล้ขอบ แถวเหล่านั้นจะแคบกว่าและซ่อนอยู่ในส่วนโค้ง

  • นั่นคือ TransformingLazyColumn

  • แถวจะเปลี่ยนรูปแบบเพื่อไม่ให้ขอบหน้าปัดทรงกลมตัดข้อความออก

  • หน้าจอแบบไดนามิกอาจทำให้การสแกนเป็นเรื่องยาก

  • ข้อความใหม่จะเข้ามา แถวจะเลื่อน และบับเบิลจะมีทุกรูปแบบ

  • มาดูกันว่าการทำอย่างถูกต้องหมายความว่าอย่างไร

  • หากคุณสังเกตเห็นทางด้านซ้าย ฟองอากาศจะหดตัวลงอย่างรวดเร็วเกินไป

  • ข้อความแต่ละข้อความภายในจะเป็นบับเบิลที่รวมอยู่ในการ์ด

  • เราบอกให้การ์ดหลักใช้การเปลี่ยนรูปแบบ แต่ระบบก็ส่งการเปลี่ยนรูปแบบเดียวกันไปยังบับเบิลย่อยด้วย

  • ด้วยเหตุนี้ บับเบิลย่อยจึงได้รับการเปลี่ยนรูปแบบตามที่ต้องการเป็น 2 เท่า

  • การแก้ไขคือตรวจสอบว่าเราใช้การเปลี่ยนรูปแบบที่ระดับเดียว ซึ่งก็คือที่การ์ดหลัก

  • และนั่นก็เป็นหลักการสำคัญเช่นกัน

  • เราตรวจสอบแล้วว่ามีการแปลง 1 รายการสำหรับสินค้า 1 รายการ

  • และยังมีปัญหาอีกอย่างหนึ่งคือ บับเบิลมีขนาดเล็กและจางลงตรงกลางหน้าจอซึ่งควรจะมีขนาดเต็ม

  • ขนาดจะขึ้นอยู่กับตำแหน่งบนหน้าจอ

  • ในเวอร์ชันแรก เราบันทึกการปรับขนาดโดยใช้ฟังก์ชัน remember ของ Compose แต่ก็ทำให้บันทึกตำแหน่งเดิมด้วย

  • ด้วยเหตุนี้ เมื่อมีข้อความใหม่เข้ามาและเลื่อนบับเบิลเก่าขึ้น การเปลี่ยนรูปแบบของบับเบิลจึงยังคงอิงตามตำแหน่งเดิม

  • และนั่นทำให้เกิดข้อบกพร่องดังกล่าว

  • เพื่อแก้ไขปัญหานี้ เราจึงหยุดบันทึกขนาดโดยใช้การเรียกใช้ "remember" และกำจัดการเรียกใช้ดังกล่าว

  • และนั่นก็เป็นสิ่งที่ได้เรียนรู้เช่นกัน

  • เมื่อแถวย้าย การเปลี่ยนรูปแบบของแถวก็ต้องย้ายไปด้วย

  • คุณจะเห็นการแก้ไขทั้ง 2 รายการในโค้ดได้ที่นี่

    // 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)
    }
  • ที่ด้านซ้ายบน คุณจะเห็นว่ามีการใช้การเปลี่ยนรูปแบบที่ 2 ระดับ และที่ด้านขวาบน มีการใช้การเปลี่ยนรูปแบบที่ระดับเดียวเท่านั้น ซึ่งก็คือการ์ด M3

  • ที่ด้านซ้ายล่าง เราใช้การเรียกใช้ remember เพื่อแคชการเปลี่ยนรูปแบบ แต่ก็แคชตำแหน่งด้วย

  • และที่ด้านขวาล่าง เราจะคำนวณการเปลี่ยนรูปแบบโดยพิจารณาตำแหน่งล่าสุดของฟองอากาศทุกครั้ง

  • อย่างที่คุณเห็น ผลลัพธ์ที่ได้คือประสบการณ์การใช้งานที่สอดคล้องกันในทุกหน้าจอ โดยเนื้อหาหลักจะอยู่ใกล้กับตรงกลาง และแถวจะเลื่อนไปตามธรรมชาติใกล้กับขอบ

การจัดการการแจ้งเตือนบนข้อมือ

  • ต่อไปเรามาพูดถึงการแจ้งเตือนกัน

  • ทีนี้ลองจินตนาการว่าข้อมือของคุณสั่น คุณจึงดูนาฬิกาและเห็นข้อความ

  • แต่หลังจากนั้นไม่นาน ข้อมือของคุณก็สั่นอีกครั้งและอีกครั้ง

  • คุณได้รับการแจ้งเตือนหลายรายการ และเมื่อตรวจสอบแต่ละครั้ง คุณจะเห็นสิ่งเดียวกัน

  • น่ารำคาญมากเลย

  • เพื่อแก้ไขปัญหานี้ เราจึงถามตัวเอง 2 ข้อ

  • เหตุการณ์ใดที่ควรแจ้งเตือนผู้ใช้

  • และเหตุการณ์ใดควรอัปเดตการแจ้งเตือนแบบไม่มีเสียง

  • ข้อบกพร่องนี้เกิดขึ้นเนื่องจากเบื้องหลังการอัปเดตรูปโปรไฟล์ การดาวน์โหลดสื่อ และใบเสร็จ ซึ่งควรจะอัปเดตการแจ้งเตือนแบบเงียบๆ กลับแจ้งเตือนผู้ใช้แทน

  • ตอนนี้เมื่อมีข้อความใหม่เข้ามา นาฬิกาจะสั่นเพียงครั้งเดียว ส่วนเหตุการณ์ที่ไม่จำเป็นต้องแจ้งเตือนจะรีเฟรชการแจ้งเตือนแบบไม่มีเสียง

  • และนั่นก็เป็นบทเรียนด้วย

  • เมื่อการแจ้งเตือนเป็นหนึ่งในเส้นทางของผู้ใช้หลัก เราต้องตรวจสอบว่าการแจ้งเตือนแต่ละรายการสมควรได้รับความสนใจจากผู้ใช้อย่างแท้จริง

  • ในหัวข้อการแจ้งเตือน

  • ก่อนหน้านี้ การแจ้งเตือนของ WhatsApp Wear OS ช่วยให้คุณทำสิ่งต่างๆ ได้ 2 อย่าง

  • คุณจะอ่านข้อความหรือเปิดแอปก็ได้

  • หากต้องการตอบกลับการแจ้งเตือนโดยตรง คุณจะไม่สามารถทำได้ก่อนหน้านี้

  • เราจึงแก้ปัญหานี้โดยใช้การดำเนินการแจ้งเตือนของ Android กับ RemoteInput API

  • ตอนนี้ผู้ใช้สามารถพิมพ์ พูด หรือแตะคำตอบด่วนได้แล้ว

  • แต่อย่าลืมว่าการกดส่งไม่ได้หมายความว่างานเสร็จแล้ว

  • หากส่งข้อความสำเร็จ เราจะทำเครื่องหมายแชทว่าอ่านแล้วและล้างการแจ้งเตือน

  • หากส่งข้อความไม่สำเร็จ เรามีตัวเลือกให้ลองส่งอีกครั้งด้วยการแตะเพียงครั้งเดียว

  • และนั่นก็เป็นบทเรียนสำคัญขณะพัฒนาฟีเจอร์นี้ด้วย นั่นคือการดำเนินการแจ้งเตือนต้องได้รับการปฏิบัติเหมือนเป็นเวิร์กโฟลว์แบบครบวงจรที่สมบูรณ์

การย้ายข้อมูลจากไทล์ไปยังวิดเจ็ต

  • ถัดไปคือการ์ดและวิดเจ็ต

  • ไทล์คือหน้าจอแบบเต็มหน้าจอ 1 หน้า ซึ่งคุณเข้าถึงได้โดยการปัดจากหน้าปัด

  • ในทางกลับกัน วิดเจ็ตคือการ์ดขนาดเล็กหลายใบที่สามารถใส่ไว้ในหน้าจอที่เลื่อนได้หน้าจอเดียว

  • ทั้งการ์ดและวิดเจ็ต WhatsApp ทำงานโดยใช้ข้อมูลพื้นฐานเดียวกัน

  • และนั่นก็เป็นจุดหักมุมด้วย

  • คุณอาจคิดว่า UI นี้วาดโดยแอปของเรา แต่จริงๆ แล้วไม่ใช่

  • เราเพียงอธิบาย UI และข้อมูล แล้วระบบจะวาด UI และข้อมูลนั้น

  • นอกจากนี้ การแตะไอคอนใดๆ จะไม่เรียกใช้โค้ดของแอป

  • คุณขอให้เปิดหน้าจอได้เท่านั้น

  • และนี่คือบทเรียนของเรา

  • หากระบบกำลังวาด UI ของคุณ UI ดังกล่าวจะเป็นส่วนหนึ่งของผลิตภัณฑ์และเมตริกการทดสอบ

  • Frank และ Yuri จะเจาะลึกเรื่องวิดเจ็ตในการพูดคุยของพวกเขา

ป้องกันไม่ให้ผู้ใช้ที่ใช้งานอยู่ลดลงอย่างรวดเร็วด้วยการช่วยเตือนให้อัปเดต

  • และนั่นนำเรามาสู่สไลด์สุดท้าย ซึ่งก็คือการย้อนกลับไปถามตัวเองว่า ท้ายที่สุดแล้ว เราต้องการให้ผู้ใช้ใช้แอปของเรามากขึ้นเรื่อยๆ

  • ทีนี้ลองจินตนาการว่าในฐานะนักพัฒนาแอป คุณเข้าสู่ระบบแดชบอร์ดในวันหนึ่ง แล้วเห็นว่าจำนวนผู้ใช้ที่ใช้งานอยู่ลดลงอย่างมาก

  • ก่อนอื่นเลย เรื่องนี้เป็นเรื่องที่น่ากังวลอย่างมาก

  • จากนั้นคุณอาจเริ่มแก้ไขข้อบกพร่อง

  • และคุณอาจรู้สึกว่าระบบขัดข้องหรือมีรุ่นที่ไม่ดีเปิดตัวอยู่

  • แต่สำหรับเราแล้ว ไม่มีปัญหาใดเลยที่ตรงกับกรณีข้างต้น

  • ปรากฏว่าแอป WhatsApp ทุกเวอร์ชันมีวันหมดอายุในตัว 90 วัน

  • ลองนึกภาพว่าผู้ใช้ติดอยู่กับบิลด์ที่ล้าสมัย ขณะที่บิลด์ใหม่กว่าใช้เวลานานกว่าจะเข้าถึงนาฬิกาของผู้ใช้

  • ด้วยเหตุนี้ ผู้ใช้จึงไม่ได้ออกจากแอป แต่แอปก็ยังหยุดทำงานอยู่

  • เราจึงทำให้การอัปเดตแอปเป็นส่วนหนึ่งของผลิตภัณฑ์

  • ดังที่คุณเห็นบนหน้าจอ ข้อความนี้จะปรากฏบนหน้าจอ 15 วันก่อนที่แอปจะหมดอายุ

  • จากนั้นจะปรากฏอีกครั้งเมื่อเหลือเวลา 7, 5, 3, 2 และ 1 วัน

  • ผู้ใช้เลือกที่จะปิดหรือไปที่ Play Store เพื่ออัปเดตแอปก็ได้

  • ซึ่งจะช่วยแจ้งเตือนผู้ใช้หลายครั้งว่าแอปใกล้จะหมดอายุแล้ว และยังเปิดโอกาสให้ผู้ใช้อัปเดตแอปได้หลายครั้งด้วย

  • และนี่คือสิ่งสุดท้ายที่เราได้เรียนรู้

  • งานของเราในฐานะนักพัฒนาแอปยังไม่เสร็จสมบูรณ์เมื่อเราเผยแพร่อัปเดตให้ผู้ใช้ 100%

  • แต่จะเสร็จสมบูรณ์ก็ต่อเมื่อผู้ใช้ได้รับข้อมูลอัปเดตจากเราจริงๆ

  • เรามาถึงช่วงสุดท้ายแล้ว

  • หากต้องการพูดคุยต่อ โปรดเข้ามาทักทายในช่วงเวลาทำการ

  • ต่อไป Frank และ Yuri จะมาพูดถึงวิดเจ็ต 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 ใน "การปรับปรุง UI ของ 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 ใน "การปรับปรุง UI ของ 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)
}