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

2026 में Android पर संरचित समवर्तीता: scope, कैंसिलेशन, और जो लीक ये रोकती है

Android पर Kotlin संरचित समवर्तीता की एक व्यावहारिक गाइड — viewModelScope बनाम lifecycleScope, कैंसिलेशन क्यों फैलता है, और वो coroutine लीक जिन्हें यह चुपचाप रोक देती है।

MFKAPPS 6 मिनट पढ़ना

Android पर मैंने जो ज़्यादातर coroutine बग डिबग किए हैं, वे suspend फ़ंक्शन के गलत काम करने की वजह से नहीं थे — वे एक coroutine के उस चीज़ से ज़्यादा जीने की वजह से थे जिसने उसे शुरू किया था। एक नेटवर्क कॉल अपनी स्क्रीन के जाने के बाद पूरा होता है और एक नष्ट हो चुके view को अपडेट करने की कोशिश में क्रैश हो जाता है। एक Flow collector बैकग्राउंड में चलता रहता है, उस स्क्रीन के लिए बैटरी जलाते हुए जिसे अब कोई देख नहीं रहा। संरचित समवर्तीता वह अनुशासन है जो इन्हें साफ़ करना याद रखने से नहीं, बल्कि संरचना के ज़रिए ही ख़त्म कर देती है — और यह वास्तव में क्या गारंटी देती है, यह समझना यह याद रखने से ज़्यादा मूल्यवान है कि कहाँ कौन सा Scope इस्तेमाल करना है।

मूल विचार: एक coroutine अपने scope से ज़्यादा नहीं जी सकता

Kotlin में हर coroutine एक CoroutineScope से संबंधित होता है, और वह scope उसकी जीवन-अवधि तय करता है। किसी scope के अंदर एक coroutine लॉन्च करें, और वह उस scope के job का child बन जाता है। scope को कैंसिल करें, और हर child — चाहे वह कितना भी गहराई में nested हो, चाहे उसने कितने भी async कॉल में शाखा फैलाई हो — उसके साथ कैंसिल हो जाता है। पूरी गारंटी बस यही है: आप गलती से किसी coroutine को उस scope के बाहर लीक नहीं कर सकते जो उसका मालिक है, क्योंकि भाषा आपको इसका कोई तरीका ही नहीं देती।

class BudgetViewModel(private val repo: TransactionRepository) : ViewModel() {
    fun refreshMonthlyTotal() {
        viewModelScope.launch {
            val total = repo.computeMonthlyTotal() // यहाँ suspend होता है
            _monthlyTotal.value = total
        }
    }
}

अगर उपयोगकर्ता कहीं और चला जाता है और computeMonthlyTotal() के बीच में ही ViewModel clear हो जाता है, तो viewModelScope अपने आप कैंसिल हो जाता है, और ऊपर वाला coroutine रुक जाता है — यह उस लाइन तक कभी नहीं पहुँचता जो _monthlyTotal में लिखती है। जाँचने के लिए कोई मैनुअल फ़्लैग नहीं, लिखने से पहले कोई isActive गार्ड ज़रूरी नहीं, क्योंकि कैंसिलेशन के किसी suspension point तक पहुँचने के बाद coroutine बस फिर से शुरू ही नहीं होता।

viewModelScope, lifecycleScope, और सही वाला चुनना

Android कंपोनेंट की जीवन-अवधि से जुड़े दो scope देता है, और गलत वाला चुनना वह सबसे आम संरचित समवर्तीता की गलती है जो मैं देखता हूँ:

  • viewModelScope उतनी ही देर जीता है जितना ViewModel — यह configuration बदलावों (रोटेशन, डार्क मोड टॉगल) से बच निकलता है और तभी कैंसिल होता है जब ViewModel clear होता है, आमतौर पर जब उपयोगकर्ता हमेशा के लिए स्क्रीन छोड़ देता है। इसे उस काम के लिए इस्तेमाल करें जिसका परिणाम रोटेशन के बाद भी स्क्रीन को चाहिए होता है: डेटा लोड करना, किसी repository में लिखना, कोई व्युत्पन्न मान गणना करना।
  • lifecycleScope, और ख़ासकर उसके अंदर repeatOnLifecycle(Lifecycle.State.STARTED), Activity या Fragment के view lifecycle से बंधा होता है। इसे उस हर चीज़ के लिए इस्तेमाल करें जिसे स्क्रीन दिखाई न देने पर सच में रुक जाना चाहिए — सबसे आम तौर पर, UI अपडेट करने के लिए Flow collect करना:
class ReminderListFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.reminders.collect { reminders ->
                    adapter.submitList(reminders)
                }
            }
        }
    }
}

इसकी जगह सीधे viewModelScope में collect करने से collector — और वह जो भी Room query ट्रिगर करता है — ऐप के बैकग्राउंड में होने के दौरान भी चलता रहेगा, क्योंकि viewModelScope को नहीं पता कि view जा चुका है। यह क्रैश के अर्थ में लीक नहीं है; यह एक ज़्यादा शांत, धीमी कीमत है: बर्बाद CPU, बर्बाद बैटरी, और एक Flow जो ऐसे UI में दोबारा emit करता रहता है जिसे कोई देख ही नहीं सकता। repeatOnLifecycle समाधान इसीलिए है क्योंकि यह STARTED से बंधा एक ताज़ा scope फिर से derive करता है, और जैसे-जैसे lifecycle उस state के अंदर-बाहर होता है, collection को कैंसिल और फिर शुरू करता है।

संरचित समवर्तीता एरर हैंडलिंग को ईमानदार बनाती है

संरचित समवर्तीता से पहले, एक आम पैटर्न GlobalScope.launch के साथ एक नंगा coroutine छोड़ना था — बिना scope के, बिना मालिक के, किसी भी parent की निगरानी से बाहर। अगर वह exception फेंकता, तो वह कहीं उपयोगी नहीं जाता, और अगर पूरा होने तक स्क्रीन जा चुकी होती, तो क्रैश पकड़ने के लिए कुछ बचता ही नहीं था। संरचित समवर्तीता हर coroutine को एक parent रखने के लिए मजबूर करती है, जिसका मतलब है कि हर exception के पास फैलने के लिए एक तय जगह होती है: job hierarchy में ऊपर, जहाँ तक किसी ने CoroutineExceptionHandler लगाया हो, या अगर किसी ने नहीं लगाया तो scope के अपने कैंसिलेशन तक।

private val handler = CoroutineExceptionHandler { _, throwable ->
    _uiState.value = UiState.Error(throwable.message)
}

fun syncPantryFromReceipt(bitmap: Bitmap) {
    viewModelScope.launch(handler) {
        val items = ocrEngine.extract(bitmap) // exception फेंक सकता है
        repo.insertAll(items)
    }
}

यही वह पैटर्न है जिसके पीछे Stocky एक असफल रसीद स्कैन को हैंडल करता है — OCR स्टेप एक दर्जन वजहों से (खराब रोशनी, न पढ़ा जा सकने वाला फ़ॉन्ट, ऐसा रसीद फ़ॉर्मैट जो उसने कभी नहीं देखा) फेल हो सकता है, और संरचित समवर्तीता गारंटी देती है कि वह विफलता एक फेंको-और-भूल जाओ coroutine में चुपचाप गायब होने की बजाय ठीक एक जगह सामने आए।

async/await: कैंसिलेशन दोनों दिशाओं में चलता है

coroutineScope { } और async बिल्डर वही गारंटी sibling coroutine तक बढ़ाते हैं: अगर किसी coroutineScope का एक child exception फेंकता है, तो बाकी सभी child भी कैंसिल हो जाते हैं, और exception तभी बाहर फैलता है जब वे सभी रुक चुके होते हैं। यह हर उस बार मायने रखता है जब आप ऐसा समवर्ती काम बाँटते हैं जो साथ में ही अर्थपूर्ण होता है:

suspend fun loadDashboard(): DashboardState = coroutineScope {
    val totalDeferred = async { repo.computeMonthlyTotal() }
    val trendDeferred = async { repo.computeSpendingTrend() }
    DashboardState(totalDeferred.await(), trendDeferred.await())
}

अगर computeSpendingTrend() exception फेंकता है, तो computeMonthlyTotal() भी कैंसिल हो जाता है — आप कभी भी आधे-अधूरे तरीके से फेल हुए request से चुपचाप render हुए आधे dashboard के साथ नहीं फँसते। संरचित समवर्तीता के बिना, वह दूसरा async पहले वाले के क्रैश होने के बाद भी चलता रहता, उसका परिणाम किसी के इंतज़ार में नहीं होता, उसका काम बर्बाद हो जाता।

निष्कर्ष

संरचित समवर्तीता कोई स्टाइल की पसंद नहीं है — यह वह चीज़ है जो “क्या मैं इसे कैंसिल करना भूल गया” को एक runtime बग से एक compile-time असंभवता में बदल देती है। जो नियम व्यवहार में मायने रखता है वह यह है: उस scope के अंदर लॉन्च करें जो इस बात से मेल खाता हो कि परिणाम कितनी देर तक चाहिए होगा, न कि उस scope में जो बस आस-पास होता है। रोटेशन के बाद भी जिस काम की स्क्रीन को परवाह होती है उसके लिए viewModelScope, view दिखाई न देने पर जो कुछ भी रुक जाना चाहिए उसके लिए repeatOnLifecycle के साथ lifecycleScope, और जहाँ भी किसी विफलता को कहीं नहीं की बजाय एक तय जगह चाहिए वहाँ एक CoroutineExceptionHandler

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

जर्नल से और भी

MFKAPPS 5 मिनट पढ़ना

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

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

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

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

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

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

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

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

#android #engineering #kotlin