لماذا يشتكي Room من الخيط الرئيسي؟ تحليل الخطأ و3 طرق للتغلب عليه

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: لماذا يتحول "SELECT بسيط" إلى صداع

\n\n

أنت تكتب أول تطبيق لك على Android باستخدام Room و Kotlin. كل شيء يسير على ما يرام، حتى تصل إلى قراءة البيانات من قاعدة البيانات. تكتب شيئًا مثل:

\n\n
val user = userDao.getUserById(42)\ntextView.text = user.name
\n\n

تقوم بتشغيل التطبيق - فيتعطل مع الاستثناء التالي:

\n\n
java.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\n

Android هو نظام أحادي الخيط افتراضيًا. تعمل واجهة المستخدم بأكملها على الخيط الرئيسي. إذا بدأت في قراءة قاعدة البيانات على هذا الخيط، ستتجمد الواجهة حتى يكتمل الاستعلام. مع كميات كبيرة من البيانات، قد يستغرق ذلك ثوانٍ - سيرى المستخدم تطبيقًا "معلقًا".

\n\n

يتخذ Room الاحتياطات: فهو يمنع أي عمليات طويلة (قراءة، كتابة) على الخيط الرئيسي. الاستثناء هو إذا سمحت بذلك صراحةً من خلال .allowMainThreadQueries() عند بناء قاعدة البيانات. لكن لا ينبغي فعل ذلك - فهو نمط معاكس.

\n\n

إليك كيف تبدو المشكلة النموذجية:

\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 يلقي استثناءً. لنصلح الأمر!

\n\n

الطريقة 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\n
class 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 الصحيح.

\n\n

الخطوة 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\n
class MainVi                

المدونات

توصيات الكتب