expect/actual — مکانیزم Kotlin Multiplatform است که امکان اعلان APIهای وابسته به پلتفرم را در کد مشترک فراهم میکند. کلمه کلیدی expect یک قرارداد برای تابع، کلاس یا ویژگی در commonMain ایجاد میکند و کلمه کلیدی actual پیادهسازی مشخصی برای هر پلتفرم ارائه میدهد. کامپایلر بررسی میکند که برای هر اعلان expect یک پیادهسازی actual در تمام پلتفرمهای هدف وجود داشته باشد. به گفته 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 برای اشیای companion، در Kotlin 1.7 — برای کلاسهای enum، و در Kotlin 2.0 — برای typealias اضافه شد.
ویژگی کلیدی expect/actual — ایمنی در سطح کامپایلر. اگر توسعهدهنده یک اعلان expect به commonMain اضافه کند اما پیادهسازی actual را برای iOS فراموش کند، کامپایلر خطا میدهد. این کار از خطاهای runtime که برای رویکردهای بازتابی یا بارگذاری پویای کد پلتفرم مشخص هستند جلوگیری میکند.
مکانیزم 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 — از androidMain. Source setها میتوانند میانی باشند (مثلاً iosArm64Main برای معماری مشخص)، که امکان دقیقتر کردن پیادهسازیها را برای دستگاههای مختلف فراهم میکند.
// commonMain — اعلان expect
expect fun getPlatformName(): String
// androidMain — actual برای Android
actual fun getPlatformName(): String = "Android"
// iosMain — actual برای 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) |
| کلاس enum | 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 برای ذخیرهسازی امن
expect class PlatformStorage {
fun save(key: String, value: String)
fun get(key: String): String?
fun remove(key: String)
}
// androidMain — actual در 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 در iOS با 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 برای getterهای پیشپاافتاده خودداری کنید — از 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 اعلان کرد و کلاسهای مشخص در ماژولهای پلتفرمی ایجاد کرد. کارخانه یا کانتینر تزریق وابستگی پیادهسازی صحیح را در runtime ارائه میدهد. این رویکرد برای تستینگ مناسبتر است زیرا اینترفیس را میتوان mock کرد.
تزریق وابستگی (Koin، Kodein) — رویکردی انعطافپذیرتر اما کمبازدهتر. کانتینر DI به طور جداگانه برای هر پلتفرم پیکربندی میشود و وابستگیهای پلتفرمی را به کد مشترک ارائه میدهد. بر خلاف expect/actual، تزریق در runtime انجام میشود که امکان تعویض پیادهسازیها را برای تستینگ فراهم میکند. از طرف دیگر، خطاهای پیکربندی DI فقط در زمان اجرا کشف میشوند، نه در مرحله کامپیل.
| رویکرد | بررسی در مرحله کامپیل | انعطافپذیری تستینگ | هزینه اضافی runtime |
|---|---|---|---|
| expect/actual | کامل | کم (actual قابل mock نیست) | صفر (اتصال کامپیل) |
| اینترفیسها + کارخانه | جزئی | زیاد (قابل mock) | حداقل (فراخوانی مجازی) |
| تزریق وابستگی | خیر (runtime) | زیاد | متوسط (DI پراکسی) |
انتخاب بین expect/actual و جایگزینها به زمینه بستگی دارد. برای عملکرد بهینه (موتورهای بازی، پردازش بلادرنگ) expect/actual به دلیل هزینه صفر runtime ترجیح دارد. برای منطق تجاری (مخزنها، use caseها) بهتر است از اینترفیسها با DI استفاده شود تا تستینگ سادهتر شود. رویکرد ترکیبی — expect/actual برای عملیات پلتفرمی سطح پایین و اینترفیسها برای لایه منطق تجاری — در اکثر پروژههای تولیدی KMM استفاده میشود.
سوالات متداول
expect/actual پیادهسازی را در مرحله کامپیل بدون فراخوانیهای مجازی متصل میکند، در حالی که اینترفیسها — در runtime. expect/actual وجود پیادهسازی را برای همه پلتفرمها تضمین میکند، اینترفیسها نیاز به بررسیهای runtime دارند.
بله، expect enum از Kotlin 1.7 پشتیبانی میشود. همه ثابتها در expect و actual enum باید مطابقت داشته باشند. مقادیر مختلف ثابتها در پلتفرمهای مختلف — خطای کامپیل.
کامپایلر برای هر پلتفرمی که پیادهسازی actual ندارد خطا میدهد. پروژه تا زمانی که برای همه اعلانهای expect پیادهسازیهای actual مربوطه اضافه نشوند، ساخته نخواهد شد.
خیر، expect و actual باید در source setهای مختلف باشند. expect — در commonMain یا source set میانی، actual — در source set پلتفرمی. قرار دادن expect و actual در یک source set خطای کامپیل است.
برای تستینگ expect/actual از commonTest با source setهای test پلتفرمی استفاده کنید. تستهای expect را در commonTest و تستهای actual را برای هر پلتفرم بنویسید. تستهای یکپارچگی به طور جداگانه روی هر پلتفرم هدف اجرا میشوند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید