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

Android नोटिफ़िकेशन एक्शन सही तरीके से: बिना ऐप खोले एक खुराक को ली गई के रूप में चिह्नित करना

Android नोटिफ़िकेशन एक्शन बटन के लिए एक व्यावहारिक गाइड — PendingIntent, activity-trampoline प्रतिबंध, goAsync(), और Room स्टेट को सुरक्षित रूप से अपडेट करना।

MFKAPPS 5 मिनट पढ़ना

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

“बस ऐप खोलो” की समस्या

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

समाधान है NotificationCompat.Action: सीधे नोटिफ़िकेशन से जुड़े बटन जो बिना कभी किसी Activity को फ़ोरग्राउंड में लाए फ़ायर होते हैं।

val takenIntent = Intent(context, DoseActionReceiver::class.java).apply {
    action = ACTION_MARK_TAKEN
    putExtra(EXTRA_DOSE_ID, doseId)
}
val takenPendingIntent = PendingIntent.getBroadcast(
    context,
    doseId.hashCode(),
    takenIntent,
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

NotificationCompat.Builder(context, CHANNEL_ID)
    .setContentTitle("Metformin — 500mg")
    .addAction(R.drawable.ic_check, "Taken", takenPendingIntent)
    .addAction(R.drawable.ic_snooze, "Snooze", snoozePendingIntent)
    .addAction(R.drawable.ic_skip, "Skip", skipPendingIntent)
    .build()

तीन बटन, तीन PendingIntent, हर एक Activity के बजाय एक BroadcastReceiver की ओर इशारा करता है। यही अंतर पूरी तरकीब है।

Activity नहीं, receiver क्यों

एक्शन को Activity के ज़रिए भेजना लुभावना लगता है — आपके पास पहले से एक है, वह पहले से आपके डेटाबेस से बात करता है। ऐसा मत करें। Android 12 से, नोटिफ़िकेशन एक्शन से सीधे Activity लॉन्च करना (नोटिफ़िकेशन के मुख्य टैप टारगेट से नहीं) एक “activity trampoline” के रूप में प्रतिबंधित है, और जहाँ यह अभी भी काम करता है वहाँ भी इसका मतलब है एक अवांछित UI फ़्लैश, टास्क-स्विचर में एक एंट्री, और एक धीमी प्रतिक्रिया। एक BroadcastReceiver इनमें से कुछ नहीं करता — वह चलता है, अपना काम करता है, और नोटिफ़िकेशन के अपडेट होने के अलावा कुछ भी दिखाई नहीं देता।

FLAG_IMMUTABLE भी मायने रखता है: Android 12 से, mutable PendingIntent उन सभी चीज़ों के लिए डिफ़ॉल्ट रूप से ब्लॉक हैं जिन्हें स्पष्ट रूप से FLAG_MUTABLE की ज़रूरत नहीं (जैसे RemoteInput)। एक साधारण एक्शन बटन को mutability की ज़रूरत नहीं — इसे immutable चिह्नित करें और आगे बढ़ें।

receiver में असली काम करना: goAsync()

BroadcastReceiver.onReceive() के साथ दिक्कत यह है कि इससे लगभग तुरंत लौटने की उम्मीद की जाती है — कुछ मिलीसेकंड, न कि उतना समय जितना बैकग्राउंड में Room डेटाबेस में लिखने में लगता है। अगर आपका लेखन पूरा होने से पहले आप लौट जाते हैं, तो सिस्टम कुछ OEM स्किन्स पर प्रोसेस को लेखन के बीच में ही मार सकता है।

goAsync() इस अतिरिक्त समय को सही तरीके से खरीदता है:

class DoseActionReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val doseId = intent.getLongExtra(EXTRA_DOSE_ID, -1)
        val pendingResult = goAsync()

        CoroutineScope(Dispatchers.IO).launch {
            try {
                doseDao.markTaken(doseId, timestamp = System.currentTimeMillis())
                NotificationManagerCompat.from(context).cancel(doseId.toInt())
            } finally {
                pendingResult.finish()
            }
        }
    }
}

goAsync() आपको लगभग 10 सेकंड का विस्तारित जीवनकाल देता है — एक Room लेखन के लिए पर्याप्त, एक foreground service को उचित ठहराने के लिए पर्याप्त नहीं। हमेशा pendingResult.finish() को एक finally ब्लॉक में कॉल करें; किसी एक्सेप्शन पाथ पर इसे भूल जाना सिस्टम को एक ऐसा wakelock पकड़े रहने पर मजबूर करता है जिसे वह छोड़ नहीं सकता, और अंततः यह आपके receiver को दुर्व्यवहार करने वाला मान लेगा।

नोटिफ़िकेशन और डेटाबेस को ईमानदार रखना

किसी खुराक को “ली गई” के रूप में चिह्नित करना केवल एक डेटाबेस रो नहीं है — नोटिफ़िकेशन को खुद गायब या परिवर्तित होना चाहिए, वरना उपयोगकर्ता उसे फिर से टैप करेगा और सोचेगा कि कुछ क्यों नहीं हो रहा। लेखन के बाद NotificationManagerCompat.cancel() कॉल करना न्यूनतम है; स्नूज़ के लिए, मैं उसी AlarmManager/WorkManager पथ से दोबारा शेड्यूल करता हूँ जिसका मूल रिमाइंडर ने उपयोग किया था (इस शेड्यूलिंग वाले आधे हिस्से के लिए WorkManager और exact alarms वाला लेख देखें) बजाय एक दूसरी नोटिफ़िकेशन पाइपलाइन बनाने के। एक रिमाइंडर, एक कोड पथ — चाहे वह पहली बार फ़ायर हुआ हो या तीसरी बार।

दूसरी आसान गलती है ऐसी इन-मेमोरी स्टेट को अपडेट करना जिसे कुछ भी नहीं देख रहा। अगर दिन का व्यू Room से आने वाले Flow द्वारा समर्थित है, तो receiver का लेखन काफी है — अगली बार दिखाई देने पर UI खुद-ब-खुद फिर से बन जाता है, बिना किसी स्पष्ट सिंक कोड के। अगर नहीं, तो उपयोगकर्ता के ऐप में लौटते ही आपको पुराना डेटा मिलेगा, और आप अंततः एक ऐसी रेस कंडीशन को डीबग करते रह जाएँगे जो असल में सिर्फ़ एक गायब observer है।

इससे आपको क्या मिलता है

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

अगर आप किसी भी तरह का “इस पर अभी प्रतिक्रिया दो” नोटिफ़िकेशन बना रहे हैं — एक रिमाइंडर, एक टू-डू नज, एक डिलीवरी कन्फ़र्मेशन — तो यह पैटर्न डोमेन चाहे जो भी हो, लागू होता है: एक्शन को BroadcastReceiver के ज़रिए भेजें, goAsync() से अपनी विंडो बढ़ाएँ, अपनी मौजूदा डेटा लेयर के ज़रिए लिखें, और बदलाव को मैन्युअल रूप से पुश करने के बजाय एक रिएक्टिव UI को उसे उठाने दें। यह एक ऐसी सुविधा के लिए थोड़ी सी प्लंबिंग है जिसे उपयोगकर्ता तभी नोटिस करते हैं जब वह गायब हो।

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

जर्नल से और भी

MFKAPPS 8 मिनट पढ़ना

2026 में भरोसेमंद Android रिमाइंडर: WorkManager, सटीक अलार्म और नए बैटरी नियम

2026 में Android पर ऐसे रिमाइंडर कैसे शिप करें जो सचमुच फायर हों — WorkManager बनाम AlarmManager, SCHEDULE_EXACT_ALARM, POST_NOTIFICATIONS, और वे OEM झंझटें जो अब भी काटती हैं।

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

ऐसा दवा रूटीन बनाना जिस पर आप वाकई टिके रहें

दवा को समय पर लेने की एक व्यावहारिक गाइड — लोग खुराक क्यों छोड़ देते हैं, रिमाइंडर का समय असल में कैसे काम करता है, और एक ऐसा रूटीन कैसे बनाएं जो एक खराब हफ्ते में भी टिका रहे।

#reminders #ux #notifications
MFKAPPS 6 मिनट पढ़ना

एक हाइड्रेशन रूटीन जो इच्छाशक्ति पर निर्भर नहीं करता

ज़्यादातर हाइड्रेशन सलाह 'ज़्यादा पानी पिएं' पर सिमट कर रह जाती है और उम्मीद करती है कि बाकी का काम इच्छाशक्ति कर देगी। यहां एक ऐसा रूटीन है जो इच्छाशक्ति की बजाय संकेतों (cues) और हल्के रिमाइंडर्स पर बना है।

#mobile #ux #reminders