सामग्री पर जाएं
सभी पोस्ट

2026 में Jetpack Glance: एक ऐसा होम-स्क्रीन विजेट बनाना जो आपके डेटा के बारे में कभी झूठ न बोले

Jetpack Glance विजेट्स के लिए एक व्यावहारिक गाइड — स्टेट, क्लिक एक्शन्स, और वह अपडेट-कोटा जाल जो विजेट्स को पुराना डेटा दिखाने पर मजबूर कर देता है।

MFKAPPS 6 मिनट पढ़ना

एक विजेट होम स्क्रीन पर अपनी जगह तभी कमाता है जब वह सच बोले। जिस पल वह कल का आंकड़ा दिखाता है, या एक टैप तीन सेकंड तक कुछ नहीं करता, यूज़र उसे किसी फ़ोल्डर में डाल देता है और फिर कभी नहीं देखता। यह जितना लगता है उससे ऊँचा मापदंड है — मैंने जो भी विजेट कोड देखा है (मेरी अपनी पहली कोशिश समेत) वह इंस्टॉल होने के तुरंत बाद सही होता है और एक घंटे बाद चुपचाप ग़लत।

इस साल मैंने Hydrame में एक होम-स्क्रीन विजेट जोड़ा — आज की पानी की खपत, एक गिलास लॉग करने के लिए एक टैप, ऐप खोलने की ज़रूरत नहीं। यहाँ वह Jetpack Glance आर्किटेक्चर है जो इसे सटीक रखता है, और वह अपडेट-फ़्रीक्वेंसी जाल जो ज़्यादातर “मेरा विजेट अटक गया” वाली बग रिपोर्ट्स का कारण है, जो थोड़ी खोजबीन करने पर मिल जाती हैं।

RemoteViews की जगह Glance क्यों

पुराना AppWidgetProvider + RemoteViews API काम करता है, लेकिन इसका मतलब है UI को दो बार लिखना — एक बार ऐप के लिए Compose में, एक बार विजेट के लिए XML-आधारित RemoteViews में, और वह भी सपोर्टेड व्यू का एक बहुत छोटा सेट लेकर। Jetpack Glance इस बंटवारे को मिटा देता है: आप विजेट को Compose जैसे DSL में लिखते हैं, और Glance रेंडर के समय इसे आपके लिए RemoteViews में कंपाइल कर देता है।

class HydrationWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            val prefs = currentState<Preferences>()
            val consumedMl = prefs[intPreferencesKey("consumed_ml")] ?: 0
            val goalMl = prefs[intPreferencesKey("goal_ml")] ?: 2000

            GlanceTheme {
                Column(modifier = GlanceModifier.padding(12.dp)) {
                    Text("$consumedMl / $goalMl ml", style = TextStyle(fontSize = 18.sp))
                    Button(
                        text = "+ 250 ml",
                        onClick = actionRunCallback<LogGlassAction>(),
                    )
                }
            }
        }
    }
}

अगर आप सामान्य Compose के आदी हैं तो दो चीज़ें अलग दिखेंगी: provideGlance अपने खुद के लाइफ़साइकल में चलता है, ज़्यादातर समय आपके ऐप की प्रोसेस के बाहर, और जो स्टेट यह पढ़ता है वह Glance के अपने Preferences स्टोर से आता है — किसी ViewModel से नहीं, किसी StateFlow से नहीं जिसे आप मेमोरी में रख रहे हों। विजेट को कोल्ड स्टार्ट से सही ढंग से रेंडर कर पाना चाहिए — फ़ोन रीस्टार्ट होने के कुछ सेकंड बाद, और इससे पहले कि आपका ऐप एक बार भी चल चुका हो।

स्टेट सीधे Room में नहीं, एक DataStore में रहता है

Glance विजेट्स एक Activity की तरह Room के Dao से लाइव Flow नहीं रख सकते — इसे ताज़ा रखने के लिए कोई लंबे समय तक चलने वाला कलेक्टर मौजूद नहीं होता। जो पैटर्न काम करता है वह है हर विजेट के लिए एक छोटा Preferences DataStore, जिसे अंतर्निहित Room डेटा बदलने पर लिखा जाता है, और Glance के रेंडर करते समय पढ़ा जाता है:

suspend fun syncWidgetState(context: Context, dao: HydrationDao) {
    val today = dao.totalForToday()
    val manager = GlanceAppWidgetManager(context)
    val ids = manager.getGlanceIds(HydrationWidget::class.java)

    ids.forEach { id ->
        updateAppWidgetState(context, id) { prefs ->
            prefs[intPreferencesKey("consumed_ml")] = today
        }
    }
    HydrationWidget().updateAll(context)
}

syncWidgetState को हर उस लिखावट के तुरंत बाद कॉल करें जो आज का कुल बदल देती है — एक गिलास लॉग करना, कोई एंट्री एडिट करना, आधी रात का रीसेट। यही एक कॉल है जो Room (असली स्रोत) और विजेट (एक कैश्ड स्नैपशॉट) को एक-दूसरे से अलग होने से रोकती है। विजेट को एक लाइव व्यू न मानें, बल्कि एक रीड मॉडल मानें जिसे आप हर लिखावट पर रिफ़्रेश करते हैं।

अपडेट-कोटा का जाल

Android सीमित करता है कि AppWidgetManager के ज़रिए एक विजेट कितनी बार अपडेट हो सकता है — प्लेटफ़ॉर्म के दस्तावेज़ पीरियॉडिक अपडेट्स के लिए लगभग 30 मिनट की एक न्यूनतम सीमा बताते हैं, और व्यवहार में OEM बैटरी मैनेजर उस सीमा को भी अविश्वसनीय बना देते हैं। अगर आप विजेट के XML मेटाडेटा में updatePeriodMillis पर भरोसा करके मान लेते हैं कि काम हो गया, तो आपको एक ऐसा विजेट मिलेगा जो एक बार सही होगा और बाकी पूरे दिन पुराना बना रहेगा।

समाधान यह है कि विजेट को कुछ ऐसा मानना बंद करें जो पोल करता है, और इसे कुछ ऐसा मानना शुरू करें जिसे आप हर प्रासंगिक इवेंट पर पुश करते हैं:

  • updateAll() को सीधे लिखावट के रास्ते से ट्रिगर करें (ऊपर वाला कोड), किसी बैकग्राउंड शेड्यूल से नहीं।
  • WorkManager का इस्तेमाल सिर्फ़ उन अपडेट्स के लिए करें जिन्हें बिना ऐप इंटरैक्शन के वाकई होना ज़रूरी है — जैसे “आज के कुल” का आधी रात का रीसेट — और उस काम को कम बार और बैटरी के अनुकूल रखें।
  • 30 मिनट की सीमा को एक ज़्यादा टाइट पीरियॉडिक वर्कर से हराने की कोशिश न करें। आप जीत नहीं पाएंगे, और एक ऐसे नतीजे के लिए बैटरी खर्च करेंगे जिसे OS वैसे भी सीमित कर देगा।

एक बार जब विजेट सिर्फ़ असली इवेंट्स के जवाब में अपडेट होने लगता है, तो पुराने डेटा की रिपोर्ट्स लगभग गायब हो जाती हैं, क्योंकि अब कोई ऐसी विंडो नहीं बचती जहाँ विजेट “अपने अगले पोल का इंतज़ार” कर रहा हो।

क्लिक एक्शन्स एक अलग प्रोसेस में चलते हैं

actionRunCallback<LogGlassAction>() आपकी विजेट क्लास की किसी मेथड को कॉल नहीं करता — यह एक ActionCallback को ट्रिगर करता है जिसे Glance नए सिरे से इंस्टैंशिएट करता है, आपके ऐप द्वारा फ़िलहाल मेमोरी में रखी किसी भी स्टेट से इसका कोई गारंटीशुदा कनेक्शन नहीं होता:

class LogGlassAction : ActionCallback {
    override suspend fun onAction(
        context: Context,
        glanceId: GlanceId,
        parameters: ActionParameters,
    ) {
        val db = AppDatabase.get(context)
        db.hydrationDao().logGlass(amountMl = 250)
        syncWidgetState(context, db.hydrationDao())
    }
}

कॉलबैक को जिस भी डिपेंडेंसी की ज़रूरत होती है — इस उदाहरण में डेटाबेस — वह सिर्फ़ Context से रिज़ॉल्व होने लायक होनी चाहिए, क्योंकि वहाँ न कोई Activity होती है, न ViewModel, और अक्सर आपके ऐप का कोई और हिस्सा भी नहीं चल रहा होता। इसे ऐसे लिखें जैसे ऐप की प्रोसेस अभी-अभी किल कर दी गई हो, क्योंकि असली डिवाइस पर अक्सर वह किल की जा चुकी होती है।

अपना पहला विजेट जोड़ने वाले किसी भी व्यक्ति से मैं यह कहूँगा

  1. विजेट को लाइव मिरर नहीं, बल्कि एक रीड मॉडल के तौर पर डिज़ाइन करें। इसे लिखावट पर सिंक करें; इसे पोल न करने दें।
  2. हर रेंडर पर कोल्ड स्टार्ट मान लें। कोई भरोसेमंद ViewModel नहीं, कोई कैश्ड सिंगलटन नहीं — हर बार पर्सिस्टेंट स्टोरेज से पढ़ें।
  3. अपडेट की न्यूनतम सीमा से लड़ने के बजाय उसका सम्मान करें। एक विजेट जिसे लिखावट पर पुश किया जाता है, वह बिना किसी टाइट पीरियॉडिक शेड्यूल के भी सटीक बना रहता है।
  4. सिर्फ़ क्लीन इंस्टॉल के बाद ही नहीं, रीस्टार्ट और force-stop के बाद भी टेस्ट करें। ज़्यादातर विजेट बग्स वहीं छिपे होते हैं।

Hydrame का विजेट कुछ हफ़्तों से लाइव है, और जो सबक याद रह गया वह वही है जो Android के बैकग्राउंड काम में हर जगह दिखता है: प्लेटफ़ॉर्म बिना वजह मुश्किल नहीं बना रहा — यह बैटरी को उस कोड से बचा रहा है जो यह मान लेता है कि वह जब चाहे पोल कर सकता है। इसके खिलाफ़ लड़ने के बजाय इस धारणा के इर्द-गिर्द डिज़ाइन करें, और विजेट बिना ज़्यादा अतिरिक्त मेहनत के ईमानदार बना रहेगा।

// संबंधित पठन

जर्नल से और भी

MFKAPPS 5 मिनट पढ़ना

2026 में Room डेटाबेस माइग्रेशन: बिना एक भी रो खोए स्कीमा बदलाव शिप करना

Android पर Room डेटाबेस माइग्रेशन के लिए एक व्यावहारिक गाइड — AutoMigration, हाथ से लिखे Migration ऑब्जेक्ट्स, और अपने यूज़र्स के बग पाने से पहले माइग्रेशन को कैसे टेस्ट करें।

#android #engineering #room
MFKAPPS 5 मिनट पढ़ना

Subly बनाना: सब्सक्रिप्शन रिन्यूअल तारीखों के पीछे का कैलेंडर गणित

किसी सब्सक्रिप्शन की अगली चार्ज तारीख का अनुमान लगाना तब तक साधारण लगता है जब तक आप महीने के अंत की बिलिंग, लीप ईयर और ट्रायल कन्वर्शन से नहीं टकराते। यहाँ बताया गया है कि Subly इसे डिवाइस पर ही कैसे सही करता है।

#android #engineering #subscriptions
MFKAPPS 6 मिनट पढ़ना

2026 में Jetpack DataStore: बिना एक भी सेटिंग खोए SharedPreferences से माइग्रेट करना

Android पर SharedPreferences से Jetpack DataStore पर जाने की एक व्यावहारिक गाइड — असिंक्रोनस दिक्कतें, मौजूदा वैल्यू को बचाने वाला माइग्रेशन तरीका, और इसे कैसे टेस्ट करें।

#android #engineering #kotlin