ฟังวิศวกรของ WhatsApp อธิบายโดยตรงเกี่ยวกับวิธีอัปเกรดแอป Wear OS ในวงกว้าง เซสชันนี้ครอบคลุมการตัดสินใจด้านสถาปัตยกรรมและ UX ที่อยู่เบื้องหลังการผสานรวม Material 3 และการนำวิดเจ็ต Wear มาใช้
รับข้อมูลเชิงลึกโดยตรงเกี่ยวกับบทเรียน รูปแบบ และเครื่องมือที่ทีมใช้เพื่ออัปเดตแอปและมอบประสบการณ์ของผู้ใช้ที่สอดคล้องกัน
TransformingLazyColumn อย่างเหมาะสมหมายถึงการใช้การเปลี่ยนรูปแบบที่ระดับเดียวเท่านั้น และหลีกเลี่ยงการแคชตำแหน่งด้วย remember ของ Composeข้อความถอดเสียง
สวัสดีทุกคน
ฉันชื่อ 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 ของเรา
ขอบคุณ 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
ขอขอบคุณ
@Module
interface CallingModule {
@BindsOptionalOf
fun callingBackend(): CallingBackend
}
class Calls @Inject constructor(
val backend: Optional<CallingBackend>,
) {
fun start(): CallResult =
backend.getOrNull()?.start()
?: CallResult.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>,
)<!-- 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" /><!-- phone -->
<string name="…_disclosure">… Learn more</string>
<!-- wearable override, same key -->
<string name="…_disclosure">…Use your phone to learn more.</string>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())
}clearActivityBackStack()
// enumerate our own, kill each
killOtherProcesses()
// no threads alive — now safe
deleteDataDirectories()
// show logged out confirmation
firePostCleanupIntent()// 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)
}