परिचय: क्यों एक "सरल SELECT" सिरदर्द बन जाता है
\n\nआप Room और Kotlin के साथ अपना पहला Android एप्लिकेशन लिख रहे हैं। सब कुछ ठीक चल रहा है, जब तक आप डेटाबेस से डेटा पढ़ने नहीं आते। आप कुछ इस तरह लिखते हैं:
\n\nval user = userDao.getUserById(42)\ntextView.text = user.name\n\nआप इसे चलाते हैं — और एप्लिकेशन इस अपवाद के साथ क्रैश हो जाता है:
\n\njava.lang.IllegalStateException: Cannot access database on the main thread\nsince it may potentially lock the UI for a long period of time.\n\nपरिचित लगता है? Stack Overflow पर इस प्रश्न ने हज़ारों व्यूज़ और सैकड़ों टिप्पणियाँ एकत्रित की हैं। यह त्रुटि शुरुआती लोगों को डराती है, लेकिन वास्तव में यह कोई बग नहीं है — यह मूर्खता से बचाव है। Room (किसी भी समझदार ORM की तरह) आपको गलती से उपयोगकर्ता इंटरफ़ेस को ब्लॉक करने से रोकता है।
\n\nइस लेख में हम समझेंगे कि ऐसा क्यों होता है और Kotlin पर Android Room में SELECT-क्वेरीज़ को सही तरीके से कैसे निष्पादित करें। आप तीन सिद्ध दृष्टिकोण सीखेंगे: LiveData, Coroutines और RxJava। चलिए शुरू करते हैं!
\n\nमुख्य बात संक्षेप में: Room main thread को क्यों ब्लॉक करता है?
\n\nAndroid डिफ़ॉल्ट रूप से एक सिंगल-थ्रेडेड सिस्टम है। सारा UI main thread (मुख्य थ्रेड) पर काम करता है। यदि आप इस थ्रेड पर डेटाबेस पढ़ना शुरू करते हैं, तो इंटरफ़ेस तब तक फ़्रीज़ रहेगा जब तक क्वेरी पूरी नहीं हो जाती। बड़े डेटा वॉल्यूम के साथ, इसमें सेकंड लग सकते हैं — उपयोगकर्ता को एक "लटका हुआ" एप्लिकेशन दिखाई देगा।
\n\nRoom सावधानी बरतता है: यह मुख्य थ्रेड पर किसी भी लंबी कार्रवाई (पढ़ना, लिखना) को प्रतिबंधित करता है। अपवाद तब है जब आप डेटाबेस बनाते समय .allowMainThreadQueries() के माध्यम से स्पष्ट रूप से इसकी अनुमति देते हैं। लेकिन ऐसा करना उचित नहीं है — यह एक एंटी-पैटर्न है।
यहाँ एक विशिष्ट समस्या दिखती है:
\n\n// DAO\n@Dao\ninterface UserDao {\n @Query("SELECT * FROM users WHERE id = :userId")\n suspend fun getUserById(userId: Int): User // suspend — पहले से ही संकेत!\n}\n\n// Activity\nclass MainActivity : AppCompatActivity() {\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n // ❌ त्रुटि!\n val user = database.userDao().getUserById(42)\n }\n}\n\nभले ही आपने फ़ंक्शन को suspend बनाया हो, लेकिन इसे कोरूटीन के बिना कॉल करते हैं — Room फिर भी अपवाद फेंकेगा। इसे ठीक करते हैं!
दृष्टिकोण 1: LiveData — सरल और प्रतिक्रियाशील
\n\nसबसे आसान तरीका DAO से LiveData लौटाना है। Room स्वयं थ्रेड का प्रबंधन करता है: क्वेरी बैकग्राउंड में निष्पादित होती है, और परिणाम UI में आता है।
\n\nचरण 1. DAO बदलें
\n\n@Dao\ninterface UserDao {\n @Query("SELECT * FROM users WHERE id = :userId")\n fun getUserById(userId: Int): LiveData<User>\n}\n\nचरण 2. Activity/Fragment में सब्सक्राइब करें
\n\nclass MainActivity : AppCompatActivity() {\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n\n database.userDao().getUserById(42).observe(this) { user ->\n textView.text = user.name\n }\n }\n}\n\nलाभ: न्यूनतम कोड, Activity नष्ट होने पर स्वचालित अनसब्सक्राइब, प्रतिक्रियाशीलता — तालिका बदलने पर डेटा स्वयं अपडेट होता है।
\n\nनुकसान: LiveData जीवनचक्र से बंधा है, इसे अन्य डेटा स्रोतों (नेटवर्क, SharedPreferences) के साथ आसानी से संयोजित नहीं किया जा सकता।
\n\nदृष्टिकोण 2: Coroutines — आधुनिक Kotlin तरीका
\n\nयदि आप Kotlin Coroutines का उपयोग कर रहे हैं (और आपको करना चाहिए), तो यह सबसे सुरुचिपूर्ण तरीका है। फ़ंक्शन को suspend के रूप में चिह्नित किया जाता है, और कॉल को सही Dispatchers.IO के साथ कोरूटीन में लपेटा जाता है।
चरण 1. suspend के साथ DAO
\n\n@Dao\ninterface UserDao {\n @Query("SELECT * FROM users WHERE id = :userId")\n suspend fun getUserById(userId: Int): User\n}\n\nचरण 2. ViewModel से कॉल (अनुशंसित)
\n\nclass MainVi