مقدمة: لماذا يتحول "SELECT بسيط" إلى صداع
\n\nأنت تكتب أول تطبيق لك على Android باستخدام Room و Kotlin. كل شيء يسير على ما يرام، حتى تصل إلى قراءة البيانات من قاعدة البيانات. تكتب شيئًا مثل:
\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في هذه المقالة، سنشرح لماذا يحدث هذا و كيفية تنفيذ استعلامات SELECT بشكل صحيح في Android Room باستخدام Kotlin. ستتعرف على ثلاثة أساليب مجربة: LiveData و Coroutines و RxJava. هيا بنا!
\n\nباختصار عن الأمر الرئيسي: لماذا يحظر Room الخيط الرئيسي؟
\n\nAndroid هو نظام أحادي الخيط افتراضيًا. تعمل واجهة المستخدم بأكملها على الخيط الرئيسي. إذا بدأت في قراءة قاعدة البيانات على هذا الخيط، ستتجمد الواجهة حتى يكتمل الاستعلام. مع كميات كبيرة من البيانات، قد يستغرق ذلك ثوانٍ - سيرى المستخدم تطبيقًا "معلقًا".
\n\nيتخذ Room الاحتياطات: فهو يمنع أي عمليات طويلة (قراءة، كتابة) على الخيط الرئيسي. الاستثناء هو إذا سمحت بذلك صراحةً من خلال .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أسهل طريقة هي إرجاع LiveData من DAO. يدير Room الخيط بنفسه: يتم تنفيذ الاستعلام في الخلفية، وتصل النتيجة إلى واجهة المستخدم.
\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. DAO مع suspend
\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