
حل مشكلة Circular Dependency في ملف libs.versions.toml
مع تحديثات نظام البناء Gradle في بيئة تطوير أندرويد، أصبح استخدام نظام
إدارة المكتبات المركزي Version Catalog عبر ملف libs
.versions.toml هو الخيار الافتراضي والأساسي لكل المطورين. يُسهل هذا النظام إدارة
إصدارات المكتبات والمكونات الإضافية (Plugins) في مكان واحد ومشاركتها بين كتل
المشروع المختلفة وسطور البرمجة بكفاءة عالية.
ولكن، أثناء تنظيم هذا الملف وتجميع المكتبات، قد يواجه المطورون فجأة خطأً
مزعجاً يتسبب في إيقاف بناء المشروع (Build Failure) تماماً،
وهو خطأ الاعتماد الدائري (Circular Dependency).
* ما هو خطأ الـ Circular Dependency وكيف يحدث في ملف TOML؟
يحدث خطأ الاعتماد الدائري عندما تعتمد مكتبة أو مجموعة (Bundle)
على عنصر آخر، وفي نفس الوقت يعتمد هذا العنصر الآخر على العنصر الأول بشكل مباشر أو
غير مباشر، مما يخلق حلقة مفرغة لا تنتهي بالنسبة لـ Gradle.
في ملف libs.versions.toml، يحدث هذا فجأة وغالباً دون انتباه عند استدعاء مكتبة داخل
قسم المجموعات [bundles] تكون هي نفسها جزءاً من مجموعة أخرى،
أو عند محاولة جعل مكتبة (Library) تعتمد على ملحق (Plugin) يعتمد بدوره
على تلك المكتبة لتحديد إصداره. يقف Gradle هنا عاجزاً عن تحديد أي المكونات
يجب بناؤه أولاً، فيتوقف الـ Build ويظهر الخطأ.
* خطوات إصلاح خطأ Circular Dependency (في 3 خطوات عملية)
لحل هذه المشكلة وفك التشابك بين المكتبات سريعاً، اتبع الخطوات الثلاث التالية:
الخطوة 1: تحديد المكونات المتداخلة من سجل الأخطاء (Gradle Log)
لا تخمن الحل؛ افتح تبويب Build في Android Studio واقرأ رسالة الخطأ بعناية.
سيعطيك Gradle مسار الحلقة المفرغة بوضوح، على سبيل المثال:
Circular dependency between the following tasks: ... أو سيشير
إلى أسماء المجموعات المتضاربة داخل ملف الـ TOML مثل:
bundleA -> bundleB -> bundleA. استخرج الأسماء المتسببة في المشكلة تماماً.
الخطوة 2: فك التشابك وفصل الاعتماديات في كود الـ TOML
توجه إلى ملف libs.versions.toml وقم بمعالجة الأسطر المتداخلة.
إليك مثالاً عملياً لكيفية حدوث الخطأ وكيفية إصلاحه:
** الشكل الخاطئ المتسبب في الخطأ (مثال):
[versions]
retrofit = "2.9.0"
[libraries]
# هنا تكمن المشكلة إذا تم استدعاء المكتبات بشكل متداخل داخل المجموعات
network-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
[bundles]
# خطأ: تجميع مكتبات تعتمد برمجياً على بعضها بشكل دائري في كتل Gradle
network-bundle = ["network-core", "custom-interceptor"]
--
** الشكل الصحيح بعد الإصلاح الفوري:
قم بفصل المكتبة المسببة للتضارب خارج الـ bundle واجعل استدعاءها مستقلاً تماماً في ملف الـ build.gradle الخاص بالموديول:
[versions]
retrofit = "2.9.0"
[libraries]
network-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
custom-interceptor = { group = "com.example", name = "interceptor", version = "1.0.0" }
[bundles]
# الحل: الاحتفاظ بالمكتبات المستقلة فقط داخل الـ bundle وتجنب التداخل
network-bundle = ["network-core"]
--
الخطوة 3: تنظيف كاش الـ Gradle وإعادة بناء المشروع (Clean & Rebuild)
بعد تعديل وحفظ ملف الـ TOML، قد تظل النسخة القديمة مخزنة في ذاكرة الـ Android Studio.
لضمان تطبيق الحل:
- من القائمة العلوية اختر File.
- اضغط على Invalidate Caches / Restart.
- بعد إعادة التشغيل، قم بعمل Sync Project with Gradle Files ثم Rebuild Project.
ستجد أن الخطأ اختفى تماماً وتم بناء المشروع بنجاح.
لماذا يظهر خطأ الاعتماد الدائري في Version Catalog؟
عند الانتقال إلى نظام التدوين المركزي الجديد، يعتقد الكثير من المطورين أن
ملف الـ TOML هو مجرد ملف نصي لتخزين الكلمات والنسخ، لكن في الحقيقة،
يتعامل معه نظام البناء Gradle كخريطة ديناميكية متكاملة لبناء شجرة الاعتماديات
(Dependency Graph). عندما تختل هذه الخريطة، ينهار الـ Build فوراً.
* كيف يقرأ Gradle المجموعات (Bundles) والإصدارات (Versions)؟
ليتمكن Gradle من تجميع مشروعك، فإنه يمر بمرحلة تسمى Configuration Phase.
خلال هذه المرحلة، يقرأ ملف libs.versions.toml بترتيب شجري صارم:
- قسم الإصدارات [versions]: يقرأ Gradle الأرقام النصية أولاً ويخزنها في الذاكرة كمتغيرات ثابتة.
- قسم المكتبات [libraries]: يقوم بربط كل مكتبة برقم إصدارها بناءً على المرجع (version.ref).
في هذه الخطوة، تصبح كل مكتبة "عنصراً مستقلًا" ينتظر الاستدعاء.
- قسم المجموعات [bundles]: هنا يقوم Gradle بدمج العناصر المستقلة في حزمة واحدة لتسهيل استدعائها بأسطر أقل.
* الخطأ القاتل يحدث هنا؛ Gradle يقرأ المجموعات والمكتبات كخط مستمر يبدأ من
نقطة (أ) وينتهي بنقطة (ب). إذا وجد Gradle أن النقطة (ب) تعود لتطلب النقطة (أ) مجدداً،
فإنه يدخل في حلقة تكرار لانهائية (Infinite Loop). ولحماية الذاكرة من الانهيار، يتوقف تماماً ويطلق صرخته الشهيرة: Circular Dependency Detected.
* مثال على كيفية صنع حلقة مفرغة في كود TOML دون أن تشعر؟
لنأخذ مثالاً واقعياً جداً يحدث أثناء تنظيم مكتبات شبكات الاتصال (Network Stack) ومكتبات الفحص (Logging/Interceptors).
تخيل أنك قمت بإنشاء مجموعة تحت اسم square-tools وضعت فيها مكتبة Retrofit ومكتبة OkHttp. حتى الآن الوضع سليم. لكن الخطأ يحدث عندما تحاول داخل نفس الملف جعل مكتبة تعتمد على مجموعة، أو خلط سياق المجموعات برمجياً.
إليك الكود الذي يرفضه Gradle تماماً:
* كود TOML خاطئ (يسبب الشلل لنظام البناء):
[versions]
retrofit = "2.9.0"
okhttp = "4.11.0"
[libraries]
# المكتبة الأولى تطلب تحديد إصدارها بناءً على علاقة مع الحزمة
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
[bundles]
# الكارثة هنا: الحزمة تشمل المكتبتين
ui-network-bundle = ["retrofit-core", "okhttp-logging"]
# الحلقة المفرغة: محاولة بناء مكتبة تعتمد في سياقها على الـ bundle التي هي جزء منها!
# Retrofit -> ui-network-bundle -> Retrofit
--
* كيف يرى Gradle هذا الكود؟
عندما يحاول Gradle فك الحزمة ui-network-bundle ليقوم بتحميلها، يجد
بداخلها retrofit-core. وعندما يذهب ليتحقق من بنية retrofit-core يجدها تطلب
تهيئة مرتبطة بملفات متبادلة مع الحزمة نفسها أو مع مكتبة أخرى تطلب
الـ Retrofit كاعتماد مسبق داخل موديول آخر بالمشروع.
هذا التشابك يجعل نظام البناء عاجزاً عن معرفة من ينطلق أولاً: هل يقوم بتحميل المكتبة
أولاً أم الحزمة؟ ونتيجة لذلك يتوقف المشروع عن العمل تماماً.
كيف تحمي مشروعك من فخ الـ Circular Dependency مستقبلاً؟
الوقاية دائماً خير من البرمجة الارتجالية. حل المشكلة لمرة واحدة يضمن
عمل المشروع الآن، لكن اتباع معايير هندسية واضحة أثناء كتابة ملف
libs.versions.toml هو ما يضمن لك عدم تكرار هذا الخطأ المزعج مع كبر حجم المشروع وتعدد الموديولات مستقبلاً.
إليك أهم 3 نصائح وقائية يجب عليك تطبيقها كقاعدة ثابتة في مشاريعك:
1. حد من استخدام الـ [bundles] للمكتبات غير المتجانسة
من الأخطاء الشائعة حشو كتل الـ [bundles] بكل المكتبات المتقاربة لمجرد
اختصار أسطر الاستدعاء. الـ Bundle صُممت لجمع المكتبات المتجانسة تماماً والتي
تعمل كجسد واحد (مثل مكتبات Jetpack Compose أو كتل Firebase الفرعية).
عندما تضع مكتبة مستقلة (مثل مكتبة رفع صور أو مكتبة تحليل بيانات) داخل Bundle
خاصة بالشبكات، فإنك ترفع احتمالية أن تطلب تلك المكتبة اعتماداً خارجياً يعود ويطلب أحد عناصر الشبكة، وهنا تقع الكارثة.
* النصيحة: اجعل الحزم (Bundles) صغيرة ومحددة جداً، وإذا شككت في تداخل مكتبة ما، اتركها كعنصر مستقل في قسم [libraries] واستدعها بشكل منفرد.
2. افصل إدارة الملحقات (Plugins) تماماً عن كتل المكتبات
في نظام الـ Version Catalog، يميل البعض إلى دمج أرقام إصدارات الملحقات
مثل (Kotlin plugin أو Hilt plugin) مع إصدارات المكتبات البرمجية البرمجية
في كتل مشتركة، أو محاولة جعل الملحق يعتمد على مكتبة لتحديد نسخته.
الملحقات (Plugins) هي المسؤول الأول عن تهيئة البيئة قبل بناء الكود نفسه، وأي تداخل
بينها وبين المكتبات أثناء مرحلة الـ Configuration يدفع Gradle لطلب تهيئة المكتبة قبل الملحق أو العكس بشكل دائري.
* النصيحة: اعتمد دائماً على فصل الملحقات في قسم [plugins] الخاص بملف
TOML، واجعلها تستدعي إصداراتها بشكل مستقل تماماً، ولا تربط إصدار الملحق
بإصدار مكتبة برمجية أبدًا إلا إذا نصت الوثائق الرسمية للمكتبة على ذلك بشكل صريح.
3. حدث إصدارات Gradle بانتظام للحصول على تقارير أخطاء ذكية
في الإصدارات القديمة من نظام البناء Gradle، كان خطأ الاعتماد الدائري يظهر
كرسالة مبهمة وعامة جداً مثل Build failed with an exception دون ذكر التفاصيل،
مما يضطر المطور لقضاء ساعات في تخمين مكان الخطأ.
التحديثات المستمرة لنظام Gradle وبيئة Android Studio تأتي مع تحسينات
هائلة في محرك تحليل شجرة الاعتماديات (Dependency Graph Analysis).
الإصدارات الحديثة أصبحت قوية بما يكفي لتعطيك تقريراً مرئياً يرسم لك حلقة التداخل بالتفصيل ويقترح عليك الحل أحياناً.
* النصيحة: لا تترك مشروعك على إصدارات Gradle قديمة. واكب التحديثات المستقرة دائماً،
لأنها لا تمنحك سرعة في الـ Build فحسب، بل تختصر عليك وقت تصحيح الأخطاء
(Debugging) وتكشف لك التداخلات الدائرية قبل أن تتفاقم.
* مقالات ذات صلة :
الأسئلة الشائعة حول خطأ الاعتماد الدائري في ملف TOML (FAQ)
إليك مجموعة من أبرز الأسئلة التي يطرحها مطورو أندرويد حول مشاكل
الـ Version Catalog وكيفية التعامل مع تضاربات الـ Gradle:
* هل يتسبب خطأ Circular Dependency في بطء أداء التطبيق بعد نشره على المتجر؟
لا، هذا الخطأ هو خطأ بناء (Compilation Error) يمنع تصدير وتجميع التطبيق
من الأساس. لن تتمكن من استخراج ملف الـ APK أو الـ AAB إلا بعد معالجة التداخل
الدائري البرمجي بالكامل، وبالتالي لا علاقة له بأداء التطبيق النهائي عند المستخدمين.
* هل يفضل إلغاء ملف libs.versions.toml والعودة للطريقة القديمة لتجنب هذه المشاكل؟
بالطبع لا؛ نظام الـ Version Catalog يمنح مشروعك أداءً أفضل، وتنظيماً احترافيًا
للاعتماديات في مكان واحد. المشكلة لا تعود للملف نفسه بل لآلية توزيع المبرمج للمكتبات
وتداخلها داخل الـ [bundles]. وحلها يتطلب فقط تنظيم العلاقات برمجياً وليس إلغاء النظام الافتراضي لأندرويد.
* ما الفرق بين خطأ التداخل الدائري في ملف TOML وفي كود الـ Java/Kotlin؟
في ملف الـ TOML، يحدث التداخل أثناء مرحلة تهيئة المكتبات (Configuration Phase)
حيث يعجز Gradle عن معرفة أي مكتبة يحملها أولاً. أما في كود جافا أو كوتلن،
فيحدث التداخل عند الاعتماد المتبادل بين الفئات (Classes) مثل أن تطلب Class A بيانات
من Class B وتطلب Class B بيانات من Class A أثناء تشغيل التطبيق (Runtime).
* هل يمكن أن تتسبب الـ Plugins في حدوث التداخل الدائري؟
نعم، وبكثرة. يحدث هذا عندما تحاول ربط ملحق برمي (مثل Hilt أو SafeArgs)
بإصدار مكتبة معينة داخل كتلة مشتركة، فيحاول Gradle استدعاء الملحق لبناء المكتبة،
ليجد أن الملحق نفسه ينتظر رقم إصدار المكتبة ليتهيأ، وهنا تبدأ الحلقة المفرغة.
الفصل المستقل للملحقات هو الحل دائماً.
* قمت بتعديل ملف TOML ولكن لا يزال الخطأ يظهر لي، ما السبب؟
السبب يعود بنسبة كبيرة إلى ذاكرة الكاش (Cache) الخاصة بـ Android Studio
ونظام Gradle. يحتفظ النظام بالشجرة القديمة للاعتماديات لتسريع البناء؛ والحل هو
عمل Invalidate Caches / Restart ثم عمل Sync جديد لتجبر النظام على قراءة العلاقات النظيفة المعدلة.
* كيف يساعد استخدام كتل الـ [bundles] بشكل صحيح في تجنب المشكلة؟
الاستخدام الصحيح يعتمد على عدم حشو مكتبات غير متجانسة معاً. اجمع فقط المكتبات
التي تنتمي لنفس المطور وتعمل ككتلة واحدة (مثل مكتبات Compose).
تجنب تماماً وضع مكتبات تملك روابط خارجية متبادلة مع موديولات أخرى بالمشروع داخل حزمة واحدة.
الخاتمة
في النهاية، يُعد نظام الـ Version Catalog عبر ملف libs.versions.toml
خطوة هائلة لتطوير مشاريع أندرويد وإدارتها بمرونة واحترافية عالية.
مواجهة أخطاء مثل Circular Dependency لا تعني تراجعاً في أداء أدواتك،
بل هي إشارة برمجية واضحة تدعوك لإعادة تنظيم شجرة الاعتماديات وفك التشابك
بين المكتبات والـ Bundles. من خلال اتباع الخطوات الثلاث الشاملة والممارسات
الوقائية التي استعرضناها في هذا الدليل، يمكنك الآن الحفاظ على بيئة بناء Gradle نظيفة،
مستقرة، وسريعة، لتصب كامل تركيزك على كتابة كود تطبيقك البرمجي بكل ثقة وبدون معوقات تقنية!.