WhatsApp के इंजीनियर से सीधे तौर पर जानें कि उन्होंने अपने Wear OS ऐप्लिकेशन को बड़े पैमाने पर कैसे अपग्रेड किया. इस सेशन में, Material 3 को इंटिग्रेट करने और Wear विजेट को अपनाने के पीछे के आर्किटेक्चर और यूज़र एक्सपीरियंस से जुड़े फ़ैसलों के बारे में बताया गया है.
इस टीम ने अपने ऐप्लिकेशन को अपडेट करने और उपयोगकर्ताओं को बेहतर अनुभव देने के लिए, जो सबक सीखे, पैटर्न अपनाए, और टूल इस्तेमाल किए उनके बारे में सीधे तौर पर अहम जानकारी पाएं.
TransformingLazyColumn का सही तरीके से इस्तेमाल करने का मतलब है कि ट्रांसफ़ॉर्मेशन को सिर्फ़ एक लेवल पर लागू करना और Compose के remember की मदद से पोज़िशन को कैश मेमोरी में सेव करने से बचना.ट्रांसक्रिप्ट
नमस्ते.
मेरा नाम मार्टिन है.
और मैं सुभाग हूं.
हम Meta में इंजीनियर हैं और हमारी टीम ने Wear OS पर WhatsApp को उपलब्ध कराया है.
हमने हाल ही में अपने ऐप्लिकेशन को आधुनिक बनाया है.
इसलिए, आज हम शेयर किए गए कोड बेस पर माइग्रेट करने और फिर उस पर नए अनुभव बनाने से मिले सबक शेयर करने जा रहे हैं.
Wear OS पर WhatsApp की सबसे खास बात यह है कि इसे फ़ोन के बिना भी इस्तेमाल किया जा सकता है.
लिंक होने के बाद, यह WhatsApp डिवाइस के तौर पर काम करता है. यह आपके फ़ोन की तरह ही पूरी तरह सुरक्षित होता है. इसका अपना डेटाबेस होता है और यह सर्वर से कनेक्ट होता है.
इसलिए, जब आपका फ़ोन रेंज से बाहर होता है, तब भी यह वाई-फ़ाई या एलटीई पर काम करता रहता है.
हमने साल 2023 में, Wear OS पर WhatsApp को पहली बार लॉन्च किया था.
इसे Rust में लिखे गए एक छोटे और पोर्टेबल क्लाइंट की मदद से बनाया गया था.
इसकी मदद से, हम तेज़ी से प्रोडक्शन स्टेज तक पहुंच पाए.
हालांकि, यह WhatsApp का दूसरा और अलग वर्शन था. इसका मतलब है कि हर प्रोटोकॉल, एन्क्रिप्शन, और सुविधा को दो बार बनाना पड़ा.
इस वजह से, स्मार्टवॉच के लिए ऐप्लिकेशन को हमेशा अपडेट करना पड़ता था.
पोर्टेबल होने की वजह से, बैकग्राउंड को मैनेज करना भी मुश्किल हो गया.
क्लाइंट ने अपने थ्रेड, टाइमर, और सॉकेट को खुद मैनेज किया. ये ऐसी चीज़ें हैं जिन्हें Android आपके लिए मैनेज करना चाहता है.
Android पर, बैकग्राउंड में चलने वाले ऐप्लिकेशन के लिए नियम होते हैं.
बैटरी बचाने वाले मोड में होने पर, नेटवर्क कनेक्शन टूट जाता है.
प्रोसेस को कैश मेमोरी में सेव करने के बाद, आपके पास उसे चलाने के लिए सीमित समय होता है या बिलकुल समय नहीं होता.
इस समस्या से निपटने के लिए, आपको किसी सेवा, नौकरी या पुश की ज़रूरत होती है.
इसलिए, स्मार्टवॉच के लिए बने हमारे ऐप्लिकेशन को ये काम मैन्युअल तरीके से करने पड़ते थे.
हालांकि, हमारा Android कोड बेस पहले से ही ऐसा करता है.
इसलिए, हमने अपने Android कोड बेस को फिर से इस्तेमाल करने और उस पर माइग्रेट करने का फ़ैसला लिया. इस कोड बेस को अरबों डिवाइसों पर टेस्ट किया जा चुका है और यह पहले से ही Android के नियमों का पालन करता है.
इस तरह, नई सुविधाओं को दो बार नहीं बनाना पड़ता.
इसका मकसद आसान था: कारोबारी नियम को बनाए रखना और यूज़र इंटरफ़ेस (यूआई) को हटाना.
अब सिर्फ़ एक हेडलेस लाइब्रेरी बची है, जो Kotlin फ़्लो को अपने एपीआई के तौर पर इस्तेमाल करती है.
हालांकि, चुनौती यह है कि इतने बड़े ऐप्लिकेशन को एक बार में अनटैंगल नहीं किया जा सकता.
कुछ मॉड्यूल, यूज़र इंटरफ़ेस (यूआई) को इन्फ़्रास्ट्रक्चर के साथ मिक्स करते हैं.
अन्य ऐप्लिकेशन ऐसी सुविधाओं पर निर्भर करते हैं जिनकी ज़रूरत आपको 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>,
)मल्टीबाइंड पर सेट का एलान किया जा सकता है. उदाहरण के लिए, MessageHandler. इसके बाद, हर फ़ीचर मॉड्यूल, सेट मल्टीबाइंडिंग का इस्तेमाल करके अपना हैंडलर जोड़ सकता है.
अगर आपको पेमेंट की सुविधा नहीं देनी है, तो बिल्ड से पूरे पेमेंट मॉड्यूल को हटाया जा सकता है. ऐसा करने पर, इंजेक्ट किए गए सेट में वह हैंडलर शामिल नहीं होगा.
इस तरह, सुविधा काम करती है या नहीं, यह बिल्ड ग्राफ़ के लिए एक सवाल है.
हालांकि, इस डिकपलिंग के दोनों तरह के असर होते हैं.
डायरेक्ट रेफ़रंस की वजह से, किसी मॉड्यूल को मिटाने पर आपका बिल्ड काम नहीं करेगा.
वैकल्पिक या मल्टीबाइंडिंग के साथ, मॉड्यूल का रेफ़रंस देने वाली कोई चीज़ नहीं होती. इसलिए, बिल्ड पास हो जाता है और सुविधा चुपचाप गायब हो जाती है या डिफ़ॉल्ट रूप से लागू हो जाती है.
हमने ठीक यही किया.
रीफ़ैक्टर की वजह से, एक सुविधा को उसके मॉड्यूल में ले जाया गया. साथ ही, हमारे बिल्ड ऐप्लिकेशन और स्मार्टवॉच के लिए ऐप्लिकेशन में इसे शामिल नहीं किया गया.
इस वजह से, ऑडियो मैसेज की अवधि शून्य सेकंड दिख रही थी.
तो यह कैसे पक्का करें कि ऐसा न हो?
इसके लिए, आपको एक टेस्ट की ज़रूरत होगी.
हमारे मामले में, हम फ़ोन ऐप्लिकेशन और स्मार्टवॉच के लिए ऐप्लिकेशन के बीच मल्टीबाइंडिंग सेट की तुलना करते हैं. ऐसा उन इंटरफ़ेस के लिए किया जाता है जो ज़रूरी हैं.
इस तरह, हम यह पक्का करते हैं कि हमारे पास हमेशा सही तरीके से लागू किया गया सिस्टम हो.
अब ट्रैक दो में वह कॉन्टेंट शामिल है जिसे हम अब तक नहीं हटा पाए हैं.
मॉड्यूल का फिर से इस्तेमाल करने से आपका काम आसान हो जाएगा. हालांकि, मॉड्यूल में मौजूद हर चीज़ आपके मेनिफ़ेस्ट में मर्ज हो जाती है.
फ़ोन लॉन्चर की गतिविधि से, दूसरा ऐप्लिकेशन आइकॉन जुड़ जाएगा या आपके ऐप्लिकेशन के साथ, कॉन्टेंट देने वाली ऐसी इकाइयां शुरू हो जाएंगी जिनकी ज़रूरत नहीं है.
मेनिफ़ेस्ट मर्जर, 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 का इस्तेमाल नहीं करते हैं. इसलिए, अनुमतियां बनी रहती हैं और हम सूचनाओं को ब्रिज करने की सुविधा चालू कर सकते हैं. इससे फ़ोन पर सूचनाएं फिर से मिलने लगती हैं.
पहले पार्ट में बस इतना ही. अब मैं सुभाग को बुलाना चाहूंगा, ताकि वे आपको यूज़र इंटरफ़ेस (यूआई) में किए गए बदलावों के बारे में बता सकें.
धन्यवाद, मार्टिन.
शेयर किए गए इन्फ़्रास्ट्रक्चर की मदद से, हमारा मकसद WhatsApp को आपकी कलाई पर मौजूद स्मार्टवॉच में इस्तेमाल करने का बेहतरीन अनुभव देना था.
इस सफ़र का पहला हिस्सा, Material 3 पर स्विच करना था. हालांकि, पहले यह जानते हैं कि ऐसा क्यों किया गया.
गोल डिसप्ले पर हर पिक्सल मायने रखता है. इसलिए, Material 3 ने हमें सही कॉम्पोनेंट और डिज़ाइन फ़ाउंडेशन दिया, ताकि हम कलाई पर पहने जाने वाले डिवाइसों के लिए WhatsApp को आधुनिक बना सकें.
अगर आप विज़ुअल देखें, तो ऐसा लग सकता है कि यह सिर्फ़ यूज़र इंटरफ़ेस (यूआई) का अपडेट है.
दरअसल, हम Material 2 से Material 3 पर स्विच कर रहे थे. यह काम रातों-रात नहीं हो सकता था.
पुराने और नए कोड को एक स्विच के पीछे साथ-साथ रखना पड़ा.
इस वजह से, Figma से विज़ुअल तौर पर मैच होने वाली स्क्रीन पर अब भी कुछ गड़बड़ियां दिख रही थीं.
उदाहरण के लिए, क्राउन स्क्रोलिंग, वापस जाने के लिए दाईं ओर स्वाइप करना, और कुछ सबसे बुनियादी जेस्चर काम नहीं करते थे.
इन सब बातों का मतलब यह है कि डिज़ाइन सिस्टम का माइग्रेशन, सिर्फ़ विज़ुअल माइग्रेशन नहीं है, बल्कि यह व्यवहार का माइग्रेशन भी है.
अगर हम इसके व्यवहार की बात करें, तो किनारों के पास मौजूद लाइनें पतली होती हैं और कर्व में छिपी होती हैं.
यह TransformingLazyColumn है.
पंक्तियां इस तरह से बदल जाती हैं कि गोल बेज़ेल उन्हें काट न सके.
डाइनैमिक स्क्रीन की वजह से, चीज़ें मुश्किल हो सकती हैं.
नए मैसेज आते हैं, पंक्तियां मूव होती हैं, और बबल हर शेप में आते हैं.
आइए, देखते हैं कि सही तरीके से काम करने का क्या मतलब है.
अगर बाईं ओर देखें, तो बबल्स बहुत तेज़ी से सिकुड़ रहे हैं.
हर मैसेज, अंदरूनी तौर पर एक बबल होता है, जो कार्ड में रैप किया गया होता है.
हमने पैरंट कार्ड को ट्रांसफ़ॉर्मेशन लागू करने के लिए कहा था. हालांकि, वही ट्रांसफ़ॉर्मेशन चाइल्ड बबल पर भी लागू हो रहा था.
इस वजह से, चाइल्ड बबल को तय किए गए ट्रांसफ़ॉर्म से दोगुना ट्रांसफ़ॉर्म मिल रहा है.
हमने यह पक्का किया कि हम एक लेवल पर ट्रांसफ़ॉर्म लागू करें. इसके लिए, हमने पैरंट कार्ड पर ट्रांसफ़ॉर्म लागू किया.
यह भी एक अहम सिद्धांत है.
हमने यह पक्का किया कि एक आइटम के लिए एक ही ट्रांसफ़ॉर्मेशन हो.
एक और समस्या है: स्क्रीन के बीच में छोटे और धुंधले बबल दिखते हैं, जबकि उन्हें पूरे साइज़ में दिखना चाहिए.
साइज़, स्क्रीन पर उनकी पोज़िशन के हिसाब से तय होता है.
अपने शुरुआती वर्शन में, हमने Compose के रिमेंबर फ़ंक्शन का इस्तेमाल करके, साइज़ फ़िटिंग को सेव किया. हालांकि, इससे इसकी पुरानी पोज़िशन भी सेव हो गई.
नतीजतन, जब कोई नया मैसेज आता है और वह पुराने बबल्स को ऊपर की ओर शिफ़्ट करता है, तो उनका ट्रांसफ़ॉर्म अब भी उनकी पुरानी पोज़िशन पर आधारित होता है.
इस वजह से, यह गड़बड़ी होती है.
इस समस्या को ठीक करने के लिए, हमने साइज़ को सेव करने के लिए, रिमेंबर कॉल का इस्तेमाल करना बंद कर दिया. साथ ही, कॉल से छुटकारा पा लिया.
यह भी एक सीख है.
जब कोई पंक्ति मूव होती है, तो उसका ट्रांसफ़ॉर्मेशन भी उसके साथ मूव होना चाहिए.
यहां कोड में की गई दोनों गड़बड़ियों को ठीक किया जा सकता है.
// 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 कार्ड है.
सबसे नीचे बाईं ओर, हम ट्रांसफ़ॉर्म को कैश मेमोरी में सेव करने के लिए, रिमेंबर कॉल का इस्तेमाल कर रहे हैं. हालांकि, यह पोज़िशन को भी कैश मेमोरी में सेव करता है.
सबसे नीचे दाईं ओर, हम हर बार बबल की नई पोज़िशन को ध्यान में रखते हुए ट्रांसफ़ॉर्म की गिनती कर रहे हैं.
जैसा कि आप देख सकते हैं, इससे सभी स्क्रीन पर एक जैसा अनुभव मिलता है. इसमें मुख्य कॉन्टेंट, स्क्रीन के बीच में रहता है और लाइनें, स्क्रीन के किनारे पर अपने-आप मूव होती हैं.
अब बात करते हैं सूचनाओं की.
अब कल्पना करें कि आपकी कलाई पर वाइब्रेशन होता है, आप अपनी स्मार्टवॉच देखती हैं, और आपको एक मैसेज दिखता है.
हालांकि, कुछ ही देर बाद आपकी कलाई पर फिर से वाइब्रेशन होता है और यह सिलसिला चलता रहता है.
आपको कई सूचनाएं मिलती हैं और हर बार देखने पर, आपको एक ही चीज़ दिखती है.
यह तो बहुत परेशान करने वाली बात है.
इस समस्या को हल करने के लिए, हमने खुद से दो सवाल पूछे.
किस इवेंट के लिए उपयोगकर्ता को सूचना मिलनी चाहिए?
साथ ही, किस इवेंट के लिए सूचनाओं को बिना आवाज़ के अपडेट किया जाना चाहिए?
यह गड़बड़ी इसलिए हो रही थी, क्योंकि बैकग्राउंड में प्रोफ़ाइल फ़ोटो अपडेट, मीडिया डाउनलोड, और रसीदें, जो सूचना को बिना किसी आवाज़ के अपडेट करती हैं, वे उपयोगकर्ता को सूचना दे रही थीं.
अब नया मैसेज आने पर, स्मार्टवॉच सिर्फ़ एक बार वाइब्रेट होती है. वहीं, जिन इवेंट के लिए सूचना देने की ज़रूरत नहीं होती उनके लिए सूचनाएं बिना किसी आवाज़ के अपडेट हो जाती हैं.
यह भी एक सबक है.
अगर सूचनाएं भेजना आपके ऐप्लिकेशन की मुख्य सुविधाओं में से एक है, तो हमें यह पक्का करना होगा कि हर सूचना उपयोगकर्ता का ध्यान खींचने लायक हो.
सूचनाओं के बारे में.
पहले, Wear OS पर WhatsApp की सूचनाओं से दो काम किए जा सकते थे.
मैसेज को पढ़ा जा सकता है या ऐप्लिकेशन को खोला जा सकता है.
पहले, किसी सूचना का जवाब सीधे तौर पर नहीं दिया जा सकता था.
इसलिए, हमने RemoteInput API के साथ Android के सूचना ऐक्शन का इस्तेमाल करके, इस समस्या को हल किया.
उपयोगकर्ता अब जवाब टाइप कर सकता है, बोलकर दे सकता है या पहले से तैयार जवाब पर टैप कर सकता है.
हालांकि, याद रखें कि ईमेल भेजना ही काफ़ी नहीं है.
मैसेज भेजने के बाद, हम चैट को 'पढ़ा गया' के तौर पर मार्क कर देते हैं और सूचना हटा देते हैं.
अगर कोई मैसेज डिलीवर नहीं हो पाता है, तो हम उसे फिर से भेजने के लिए एक टैप की सुविधा देते हैं.
इसे डेवलप करते समय, हमने यह भी सीखा कि सूचना से जुड़ी कार्रवाई को पूरी तरह से एंड-टू-एंड वर्कफ़्लो के तौर पर माना जाना चाहिए.
इसके बाद, टाइल और विजेट.
जैसा कि आप देख सकते हैं, टाइल एक फ़ुल स्क्रीन होती है. इस पर जाने के लिए, आपको वॉच फ़ेस से स्वाइप करना होगा.
वहीं दूसरी ओर, विजेट कई छोटे कार्ड होते हैं. इनमें से कई कार्ड, स्क्रोल की जा सकने वाली एक स्क्रीन पर फ़िट हो सकते हैं.
WhatsApp टाइल और विजेट, दोनों एक ही डेटा का इस्तेमाल करते हैं.
यह भी एक ट्विस्ट है.
आपको लग सकता है कि यह यूज़र इंटरफ़ेस (यूआई) हमारे ऐप्लिकेशन से बनाया जा रहा है. हालांकि, ऐसा नहीं है.
हम सिर्फ़ यूज़र इंटरफ़ेस (यूआई) और डेटा के बारे में बताते हैं. इसके बाद, सिस्टम उसे ड्रॉ करता है.
इसके अलावा, किसी भी आइकॉन पर टैप करने से आपके ऐप्लिकेशन का कोड नहीं चलाया जा सकता.
इसे सिर्फ़ कोई स्क्रीन खोलने के लिए कहा जा सकता है.
इस लेसन में बस इतना ही.
अगर सिस्टम आपके यूज़र इंटरफ़ेस (यूआई) को ड्रॉ कर रहा है, तो यह आपके प्रॉडक्ट और टेस्टिंग मेट्रिक का हिस्सा है.
फ़्रैंक और यूरी, विजेट के बारे में ज़्यादा जानकारी देंगे.
अब हम आखिरी स्लाइड पर आ गए हैं. इसमें हम एक कदम पीछे हटकर खुद से पूछेंगे कि क्या हमें अपने ऐप्लिकेशन के लिए ज़्यादा से ज़्यादा उपयोगकर्ता चाहिए.
अब मान लें कि एक डेवलपर के तौर पर, आपने एक दिन अपने डैशबोर्ड में लॉग इन किया और आपको सक्रिय उपयोगकर्ताओं की संख्या में काफ़ी गिरावट दिखी.
सबसे पहले, यह बहुत चिंता की बात होगी.
इसके बाद, उसे डीबग करना शुरू किया जा सकता है.
आपको लग सकता है कि ऐप्लिकेशन क्रैश हो गया है या कोई खराब रिलीज़ चल रही है.
हालांकि, हमारे लिए इनमें से कोई भी वजह नहीं थी.
WhatsApp ऐप्लिकेशन के हर वर्शन की समयसीमा 90 दिनों की होती है.
ऐसे में, हो सकता है कि उपयोगकर्ता पुराने बिल्ड का इस्तेमाल कर रहे हों और नए बिल्ड को उनकी स्मार्टवॉच तक पहुंचने में समय लग रहा हो.
इस वजह से, उपयोगकर्ता ऐप्लिकेशन को बंद नहीं कर रहे हैं, लेकिन फिर भी उनका ऐप्लिकेशन काम करना बंद कर देता है.
इसलिए, हमने ऐप्लिकेशन को अपडेट करने की सुविधा को प्रॉडक्ट का हिस्सा बना दिया.
जैसा कि स्क्रीन पर दिख रहा है, ऐप्लिकेशन की समयसीमा खत्म होने से 15 दिन पहले, स्क्रीन पर यह मैसेज दिखता है.
इसके बाद, यह सूचना 7, 5, 3, 2, और 1 दिन बचे होने पर फिर से दिखती है.
उपयोगकर्ता, सूचना को खारिज कर सकता है या ऐप्लिकेशन को अपडेट करने के लिए Play Store पर जा सकता है.
इससे उपयोगकर्ता को कई रिमाइंडर मिलते हैं कि उनके ऐप्लिकेशन की समयसीमा खत्म होने वाली है. साथ ही, उन्हें अपने ऐप्लिकेशन को अपडेट करने के कई मौके भी मिलते हैं.
यह हमारी आखिरी सीख है.
डेवलपर के तौर पर हमारा काम, किसी अपडेट को 100% शिप करने पर खत्म नहीं होता.
यह प्रोसेस तब पूरी होती है, जब उपयोगकर्ताओं को हमारे अपडेट मिल जाते हैं.
इसी के साथ, हमारा यह वीडियो खत्म होता है.
अगर आपको बातचीत जारी रखनी है, तो कृपया ऑफ़िस के खुले होने के दौरान हमसे संपर्क करें.
इसके बाद, फ़्रैंक और यूरी, 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)
}