expect/actual هي آلية Kotlin Multiplatform التي تسمح بتصريح API تعتمد على المنصة في الكود المشترك. تنشئ كلمة expect عقداً لدالة أو فئة أو خاصية في commonMain، بينما تقدم كلمة actual تنفيذًا محددًا لكل منصة. يتحقق المجمع من وجود تنفيذ actual مقابل كل تصريح expect على جميع المنصات المستهدفة. وفقًا لـ JetBrains, 2025، تستخدم هذه الآلية في 80% من مشاريع KMM لتنفيذ منطق الأعمال عبر المنصات.
النقاط الرئيسية
expect/actual هي آلية تصريحية في Kotlin Multiplatform لتنفيذ البرمجة الموجهة للمنصة. تسمح بوصف API مرة واحدة في الوحدة المشتركة (expect) وتنفيذها بشكل منفصل لكل منصة (actual). بخلاف الواجهات، لا تنشئ expect/actual استدعاءات افتراضية — يربط المجمع تصريحات expect و actual في وقت الترجمة، مما يلغي التكلفة الإضافية للإرسال الديناميكي.
بدأت قصة expect/actual مع ظهور Kotlin Multiplatform في 2017. في البداية، كانت الآلية تسمى expect/actual declarations وكانت تجريبية. في Kotlin 1.2 تمت إضافة الأنوتاتات expect، وفي Kotlin 1.3 أصبح expect/actual مستقرًا للفئات والدوال. بمرور الوقت توسعت الآلية: أضاف Kotlin 1.6 دعم expect/actual للكائنات المرافقة، Kotlin 1.7 للفئات التعدادية، و Kotlin 2.0 لـ typealias.
السمة الرئيسية لـ expect/actual هي الأمان في وقت الترجمة. إذا أضاف المطور تصريح expect في commonMain ولكن نسي تقديم تنفيذ actual لـ iOS، سيصدر المجمع خطأً. هذا يمنع أخطاء وقت التنفيذ الشائعة في الأساليب التي تستخدم الانعكاس أو التحميل الديناميكي لكود المنصة.
تعمل آلية expect/actual على مستوى source set — نظام وحدات Kotlin Multiplatform. الكود المشترك المتاح لجميع المنصات يقع في source set commonMain. الكود المعتمد على المنصة يقع في iosMain و androidMain و macosMain وغيرها. تصرح كلمة expect في commonMain بـ API، بينما تقدم كلمة actual في source set المنصة التنفيذ. يربطهما المجمع في مرحلة توليد الكود، مستبدلًا استدعاء دالة expect بتنفيذ actual المقابل للمنصة المستهدفة.
تبدو هيكل التسلسل الهرمي لـ source set في مشروع KMM نموذجي كالآتي: commonMain يحتوي تصريحات expect، iosMain و androidMain يحتويان تنفيذات actual. عند الترجمة لـ iOS، يتم استخدام actual من iosMain، وعند الترجمة لـ Android يتم استخدام actual من androidMain. يمكن أن تكون source sets وسيطة (على سبيل المثال iosArm64Main لمعمارية محددة)، مما يسمح بتحسين التنفيذات لأجهزة مختلفة.
// commonMain — expect declaration
expect fun getPlatformName(): String
// androidMain — actual for Android
actual fun getPlatformName(): String = "Android"
// iosMain — actual for iOS
actual fun getPlatformName(): String = "iOS"
يتحقق مجمع Kotlin من عدة شروط عند العمل مع expect/actual. يجب أن يكون لكل تصريح expect تنفيذ actual لكل منصة نشطة. يجب أن تتطابق توقيع actual مع توقيع expect (يمكن للتعليق @OptionalExpectation تخفيف هذا المتطلب). يجب أن تكون محددات الوصول ونوع العودة والمعاملات متطابقة. ويتحقق المجمع أيضًا من عدم وجود تبادلات دورية بين تصريحات expect و actual.
expect/actual تدعم عدة أنواع من التصريحات. الأكثر شيوعًا هي دوال expect/actual لعمليات المنصة، فئات expect/actual للكائنات التي تتطلب تنفيذًا أصليًا، وخواص expect/actual للثوابت والإعدادات. لكل نوع قواعد استخدام وقيود خاصة.
دوال expect/actual هي أبسط الأنواع وأكثرها شيوعًا. تستخدم لاستدعاء API المنصة مثل الحصول على الوقت أو قراءة الملفات أو إرسال طلبات HTTP. تستخدم فئات expect/actual لإنشاء كائنات تتفاعل مباشرة مع الكود الأصلي (على سبيل المثال للوصول إلى الكاميرا أو الموقع الجيوماني أو تخزين المفاتيح). خواص expect/actual (val) مناسبة لثوابت المنصة — اسم نظام التشغيل أو إصدار SDK أو مسار دليل النظام.
| نوع التصريح | الكلمات المفتاحية | مثال استخدام |
|---|---|---|
| دالة | expect fun / actual fun | الحصول على معرف فريد للجهاز |
| فئة | expect class / actual class | الوصول إلى SecureStorage (Keychain / EncryptedSharedPreferences) |
| خاصية | expect val / actual val | المنصة الحالية (iOS / Android) |
| فئة تعدادية | expect enum / actual enum | قائمة أذون التطبيق المتاحة |
| Typealias | expect typealias / actual typealias | نوع استجابة الشبكة المخصص للمنصة |
لا يمكن استخدام جميع بنيات Kotlin مع expect/actual. لا يمكن لـ تصريح expect أن يحتوي على جسم — فقط توقيع. لا يمكن لفئة expect أن يكون لها منشئ بمعلمات (يجب أن يكون لها منشئ أساسي فارغ). لـ enum expect/actual، يجب أن تكون جميع الثوابت متطابقة في كل من expect و actual. يجب أن تكون خواص expect من نوع val (ليس var)، لأن تخزين الحالة في الوحدة المشتركة لخواص المنصة ليس له معنى.
دعنا نستعرض أمثلة عملية لـ expect/actual من دوال بسيطة إلى فئات متكاملة. الحالة الأساسية هي الحصول على اسم المنصة لاستخدامه في واجهة المستخدم. تشمل الأمثلة الأكثر تعقيدًا الوصول إلى التخزين الأصلي والعمل مع خيوط المنصة.
// commonMain — expect class for secure storage
expect class PlatformStorage {
fun save(key: String, value: String)
fun get(key: String): String?
fun remove(key: String)
}
// androidMain — actual on Android
actual class PlatformStorage {
private val prefs = AppContext.getSharedPreferences("secure", 0)
actual fun save(key: String, value: String) { prefs.edit().putString(key, value).apply() }
actual fun get(key: String): String? = prefs.getString(key, null)
actual fun remove(key: String) { prefs.edit().remove(key).apply() }
}
في هذا المثال، تحدد فئة expect PlatformStorage عقد تخزين بسيط من نوع مفتاح-قيمة. في Android، تستخدم التنفيذ SharedPreferences، بينما في iOS تستخدم Keychain أو NSUserDefaults. بفضل expect/actual، يقوم منطق الأعمال في commonMain باستدعاء save/get/remove دون معرفة تنفيذ المنصة.
// iosMain — actual on iOS with Keychain
actual class PlatformStorage {
actual fun save(key: String, value: String) {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key,
kSecValueData to value.encodeToByteArray()
)
SecItemAdd(query, null)
}
actual fun get(key: String): String? {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key,
kSecReturnData to true
)
val result = mutableMapOf<String, Any>()
return if (SecItemCopyMatching(query, result) == errSecSuccess)
result[kSecValueData]?.toString()
else null
}
actual fun remove(key: String) {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key
)
SecItemDelete(query)
}
}
عند تصميم API expect/actual، يجب اتباع عدة مبادئ. قلل عدد تصريحات expect — كلما كان الكود المشترك أكبر، كانت الصيانة أسهل. استخدم expect/actual فقط لـ API التي تختلف حقيقيًا بين المنصات. باقي الكود، استخدم واجهات مع مصانع أو حقن التبعيات، وهو ما يبسط الاختبار.
يوصى بتجميع تصريحات expect حسب الوحدات الموضوعية، بدلاً من خلطها في ملف واحد. على سبيل المثال، Storage.kt لتصريحات expect التخزين، Platform.kt لدوال expect التي تعمل مع نظام التشغيل، و Analytics.kt لفئات expect التحليل. هذا يبسط التنقل وفهم سطح المنصة لمشروع KMM. يجب أن يكون كل ملف actual في source set المقابل: androidMain و iosMain و desktopMain وغيرها.
التنفيذات الافتراضية عبر expect fun مع actual fun حيث يستخدم actual كودًا مشتركًا هو نمط ضد الأنماط شائع. إذا لم يختلف تنفيذ المنصة عن الافتراضي، فلا حاجة لـ expect/actual. في مثل هذه الحالات، استخدم دالة بسيطة في commonMain. كما تجنب expect/actual لـ getters بسيطة — استخدم expect val مع ثوابت.
الهيكل المناسب لكود expect/actual أساسي لقراءة المشروع. يجب أن يكون لكل وحدة expect/actual نقطة دخول واحدة. مثال تنظيم: commonMain/kotlin/com/project/platform يحتوي تصريحات expect، androidMain/kotlin/com/project/platform يحتوي actual لـ Android، iosMain/kotlin/com/project/platform يحتوي actual لـ iOS. يجب أن تتطابق أسماء الملفات والحزم لـ expect و actual، لكي يتمكن المطور من العثور على التنفيذ المقابل بسرعة.
الواجهات مع مصنع المنصة هي البديل الرئيسي لـ expect/actual. بدلاً من فئة expect، يمكنك إعلان واجهة في commonMain وإنشاء فئات محددة في وحدات المنصة. يقوم مصنع أو حاوية حقن التبعيات بتوفير التنفيذ الصحيح في وقت التنفيذ. هذا النهج أنسب للاختبار، حيث يمكن محاكاة الواجهة.
حقن التبعيات (Koin, Kodein) هو نهج أكثر مرونة ولكن أقل كفاءة. يتم تكوين حاوية DI بشكل منفصل لكل منصة وتوفر تبعيات المنصة للكود المشترك. بخلاف expect/actual، يحدث الحقن في وقت التنفيذ، مما يسمح بتبادل التنفيذات للاختبار. من ناحية أخرى، لا تكتشف أخطاء تكوين DI إلا في وقت التنفيذ، وليس في وقت الترجمة.
| النهج | التحقق في وقت الترجمة | مرونة الاختبار | التكلفة الإضافية في التنفيذ |
|---|---|---|---|
| expect/actual | كاملة | منخفضة (actual لا يمكن محاكاتها) | صفرية (ربط في وقت الترجمة) |
| واجهات + مصنع | جزئية | عالية (يمكن محاكاتها) | أدنى (استدعاء افتراضي) |
| حقن التبعيات | لا (وقت التنفيذ) | عالية | متوسطة (وكلاء DI) |
يعتمد الاختيار بين expect/actual والبدائل على السياق. للكود الحاسم للأداء (محركات الألعاب، المعالجة الفورية)، expect/actual مفضل بسبب عدم وجود تكلفة إضافية. لـ منطق الأعمال (المستودعات، حالات الاستخدام)، من الأفضل استخدام واجهات مع DI لتبسيط الاختبار. نهج مجمع — expect/actual لعمليات المنصة منخفضة المستوى وواجهات لطبقة منطق الأعمال — يستخدم في معظم مشاريع KMM الإنتاجية.
الأسئلة الشائعة
expect/actual تربط التنفيذ في وقت الترجمة بدون استدعاءات افتراضية، بينما تربط الواجهات في وقت التنفيذ. expect/actual تضمن وجود تنفيذ لجميع المنصات، الواجهات تتطلب فحوصات في وقت التنفيذ.
نعم، expect enum مدعوم منذ Kotlin 1.7. يجب أن تكون جميع الثوابت متطابقة في كل من expect و actual. قيم الثوابت المختلفة على منصات مختلفة هي خطأ في الترجمة.
سيصدر المجمع خطأً لكل منصة حيث يفتقد التنفيذ actual. لن يتم بناء المشروع حتى يتم إضافة تنفيذات actual المقابلة لجميع تصريحات expect.
لا، expect و actual يجب أن يكونا في source sets مختلفة. expect في commonMain أو source set وسيط، actual في source set منصة. وضع expect و actual في نفس source set هو خطأ في الترجمة.
لاختبار expect/actual، استخدم commonTest مع source sets اختبار المنصة. اكتب اختبارات expect في commonTest واختبارات actual لكل منصة. تتم الاختبارات المكاملة بشكل منفصل على كل منصة مستهدفة.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.