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

Android ऐप शॉर्टकट्स और Quick Settings Tiles: बिना ऐप खोले पानी लॉग करना

Android के डायनामिक ShortcutManager API और TileService की एक व्यावहारिक गाइड — कैसे एक-टैप एक्शन को पूरी तरह ऐप को छोड़कर काम करने दें, साथ ही वे गड़बड़ियाँ जो ज़्यादातर इम्प्लीमेंटेशन को फँसा देती हैं।

MFKAPPS 6 मिनट पढ़ना

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

दो एंट्री पॉइंट, दो अलग-अलग काम

App shortcuts लॉन्चर आइकन के नीचे रहते हैं — उसे लॉन्ग-प्रेस करें और आपके तय किए गए एक्शन्स वाला एक मेनू खुल जाता है। ये इसी विशेष ऐप से जुड़े उच्च-आवृत्ति (high-frequency) एक्शन्स के एक छोटे समूह के लिए हैं, जिन्हें वह कोई भी खोज सकता है जिसके पास पहले से आइकन नज़र में है।

A Quick Settings Tile नोटिफ़िकेशन शेड में रहता है, Wi-Fi और Bluetooth के बग़ल में। यह उस एक एक्शन के लिए है जिसे कोई कहीं से भी उपलब्ध चाहता है, बिना किसी आइकन को ढूँढ़े — यह एक ऐप फ़ीचर से ज़्यादा एक हार्डवेयर बटन के क़रीब है।

Hydrame दोनों शिप करता है: लॉन्चर आइकन के नीचे “Log 250ml” और “Log 500ml” के लिए एक शॉर्टकट, और एक Quick Settings Tile जो लॉक स्क्रीन शेड से एक टैप में डिफ़ॉल्ट मात्रा लॉग करता है। ये अलग-अलग API हैं, अलग-अलग रजिस्ट्रेशन पथ, और इन्हें एकीकृत करने की कोशिश करने के बजाय अलग-अलग बनाना ही बेहतर है।

डायनामिक शॉर्टकट्स: ShortcutManagerCompat

शॉर्टकट्स दो प्रकार में आते हैं — static (XML में घोषित, बिल्ड टाइम पर तय) और dynamic (रनटाइम पर पुश किए गए, ऐप की स्थिति के आधार पर बदल सकते हैं)। एक स्थिर “Log 250ml” को बदलने की ज़रूरत नहीं, इसलिए static काम कर जाता, लेकिन dynamic शॉर्टकट्स मात्राओं को यह दर्शाने देते हैं कि कोई असल में क्या पीता है — वे दो साइज़ जो वह सबसे ज़्यादा इस्तेमाल करता है, न कि वे दो जिनका ऐप ने अंदाज़ा लगाया।

val shortcut = ShortcutInfoCompat.Builder(context, "log_250ml")
    .setShortLabel("250ml")
    .setLongLabel("Log 250ml of water")
    .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_glass))
    .setIntent(
        Intent(context, LogIntakeReceiverActivity::class.java).apply {
            action = ACTION_LOG_INTAKE
            putExtra(EXTRA_AMOUNT_ML, 250)
        }
    )
    .build()

ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut250, shortcut500))

इंटेंट को किसी Activity को लक्ष्य करना होगा, BroadcastReceiver को नहीं — यह वह एक सख़्त बाध्यता है जो शॉर्टकट API लागू करता है, notification-action पैटर्न के उलट जहाँ receiver सही चुनाव होता है। Google का अपना मार्गदर्शन यह है कि उस Activity को यथासंभव अदृश्य बनाया जाए: कोई लेआउट नहीं, राइट के तुरंत बाद finish()

class LogIntakeReceiverActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val amount = intent.getIntExtra(EXTRA_AMOUNT_ML, 0)

        lifecycleScope.launch {
            intakeDao.logIntake(amount, timestamp = System.currentTimeMillis())
            Toast.makeText(this@LogIntakeReceiverActivity, "Logged ${amount}ml", Toast.LENGTH_SHORT).show()
            finish()
        }
    }
}

इसे एक पारदर्शी थीम दें (Theme.Material3.DayNight.NoActionBar को windowIsTranslucent से ओवरराइड किया हुआ) और ट्रांज़िशन एक सिस्टम कन्फ़र्मेशन जैसा लगता है, ऐप लॉन्च जैसा नहीं — यह पूरी चीज़ एक मिड-रेंज डिवाइस पर आधे सेकंड से भी कम में हल हो जाती है।

रैंक और कैप: शॉर्टकट्स कोई मेनू नहीं हैं

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

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

Quick Settings Tile: TileService

एक tile एक service है, Activity नहीं — Android इसके लाइफ़साइकल को नियंत्रित करता है, और आपको बिलकुल एक ही एंट्री पॉइंट मिलता है जो मायने रखता है: onClick()

class LogWaterTileService : TileService() {
    override fun onStartListening() {
        super.onStartListening()
        qsTile?.apply {
            label = "Log water"
            icon = Icon.createWithResource(this@LogWaterTileService, R.drawable.ic_tile_glass)
            state = Tile.STATE_ACTIVE
            updateTile()
        }
    }

    override fun onClick() {
        super.onClick()
        val pendingResult = goAsync()
        lifecycleScope.launch {
            try {
                intakeDao.logIntake(defaultAmountMl(), timestamp = System.currentTimeMillis())
            } finally {
                pendingResult.finish()
            }
        }
    }
}

goAsync() यहाँ उसी वजह से दिखता है जिस वजह से यह BroadcastReceiver में दिखता है: onClick() से जल्दी लौटने की उम्मीद की जाती है, और मेन थ्रेड पर एक Room राइट शेड की कोलैप्स एनिमेशन को झटका देगी। tile को मेनिफ़ेस्ट में BIND_QUICK_SETTINGS_TILE अनुमति के साथ घोषित करना होगा, और — यही वह हिस्सा है जो लोगों को फँसाता है — यह कहीं भी अपने आप नज़र नहीं आएगा। उपयोगकर्ता को इसे शेड की एडिट स्क्रीन से खींचकर अपनी सक्रिय टाइल्स में डालना होगा। TileService.requestListeningState() Android 13+ को इसे जोड़ने का सुझाव देने के लिए प्रेरित कर सकता है, लेकिन प्लेसमेंट को ज़बरदस्ती कराने का कोई तरीक़ा नहीं है, और यह उम्मीद करने की कोई वजह नहीं कि ज़्यादातर उपयोगकर्ता इसकी फ़िक्र करेंगे। tile को एक पावर-यूज़र फ़ीचर मानें, प्राथमिक इंटरैक्शन पथ नहीं।

Tile की स्थिति को ईमानदार रखना

एक tile जो ऐप की स्थिति चाहे जो हो हमेशा वही आइकन दिखाती है, पहली बार जब स्थिति अलग होती है तो टूटी हुई लगती है। अगर Hydrame का दैनिक लक्ष्य पहले ही पूरा हो चुका है, तो onStartListening() इसे दर्शाने की जगह है:

override fun onStartListening() {
    super.onStartListening()
    lifecycleScope.launch {
        val goalMet = intakeDao.todayTotal() >= goalDao.currentGoal()
        qsTile?.apply {
            state = if (goalMet) Tile.STATE_INACTIVE else Tile.STATE_ACTIVE
            subtitle = if (goalMet) "Goal reached" else null
            updateTile()
        }
    }
}

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

यह किसलिए बनाने लायक़ है

इन दोनों API में से कोई भी मुश्किल नहीं है, बशर्ते आप वे दो नियम जान लें जो डॉक्स से साफ़ नज़र नहीं आते: शॉर्टकट्स को बैकग्राउंड राइट के लिए भी एक Activity चाहिए, और एक tile को उसी वजह से goAsync() चाहिए जिस वजह से एक notification action receiver को चाहिए। ये जो देते हैं वह कोड की मात्रा के मुक़ाबले असंगत रूप से ज़्यादा है — एक लॉन्ग-प्रेस या एक शेड टैप, उन मुट्ठीभर एक्शन्स के लिए पूरे ऐप लॉन्च की जगह ले लेता है जिन्हें कोई हर दिन दोहराता है। यह उस तरह का घर्षण है जिसे उपयोगकर्ता सचेत रूप से हटता हुआ नहीं देखते, बस अगर आप इसे वापस हटा दें तो उसकी अनुपस्थिति महसूस करते हैं। किसी भी ऐसे ऐप के लिए जिसमें एक या दो प्रमुख एक्शन हों — कुछ लॉग करना, कुछ शुरू करना, कुछ टॉगल करना — यह एक दोपहर ख़र्च करने लायक़ है।

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

जर्नल से और भी

MFKAPPS 5 मिनट पढ़ना

Android का In-App Review API: बिना परेशान किए रेटिंग माँगना

Google के In-App Review API की एक व्यावहारिक गाइड — यह असल में कैसे काम करता है, इसे कब ट्रिगर करना चाहिए, और सामान्य 'हमें रेट करें' पॉपअप आपकी Play Store रेटिंग को चुपचाप कैसे नुकसान पहुँचा रहा है।

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

2026 में Android पर फोरग्राउंड सर्विस टाइप्स: वह चुनना जो आपके फीचर पर सच में लागू हो

Android के फोरग्राउंड सर्विस टाइप प्रतिबंधों की एक व्यावहारिक गाइड — dataSync, mediaPlayback, specialUse, shortService — और बिना मारे या रिजेक्ट हुए सही वाला कैसे चुनें।

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

Jetpack Compose में StateFlow बनाम SharedFlow: रीप्ले बग के बिना UI स्टेट मॉडल करना

एक Compose ViewModel में StateFlow कब इस्तेमाल करें और SharedFlow कब — और दोनों को मिला देने पर सामने आने वाला वन-टाइम-इवेंट बग।

#android #engineering #kotlin