expect/actual — กลไกของ Kotlin Multiplatform ที่ช่วยให้ประกาศ API ที่ขึ้นกับแพลตฟอร์มในโค้ดทั่วไปได้ คีย์เวิร์ด expect สร้างสัญญาของฟังก์ชัน คลาส หรือคุณสมบัติใน commonMain ส่วนคีย์เวิร์ด actual จัดเตรียม implementation ที่เป็นรูปธรรมสำหรับแต่ละแพลตฟอร์ม คอมไพเลอร์จะตรวจสอบว่า expect-declaration ทุกตัวมี actual-implementation ที่สอดคล้องกันในทุกแพลตฟอร์มเป้าหมาย ตามข้อมูลของ JetBrains, 2025 กลไกนี้ถูกใช้ใน 80% ของโปรเจกต์ KMM เพื่อ implement โลจิกทางธุรกิจเฉพาะแพลตฟอร์ม
สิ่งสำคัญ
expect/actual — คือกลไกแบบประกาศของ Kotlin Multiplatform สำหรับการเขียนโปรแกรมเชิงแพลตฟอร์ม โดยอนุญาตให้อธิบาย API หนึ่งครั้งในโมดูลทั่วไป (expect) และ implement แยกกันสำหรับแต่ละแพลตฟอร์ม (actual) ต่างจากอินเทอร์เฟซ expect/actual ไม่สร้าง virtual call — คอมไพเลอร์เชื่อมโยง expect- และ actual-declaration ในขั้นตอนการคอมไพล์ ซึ่งช่วยลด overhead จากการ dispatch แบบไดนามิก
ประวัติของ expect/actual เริ่มต้นจากการปรากฏของ Kotlin Multiplatform ในปี 2017 เริ่มแรกกลไกนี้มีชื่อว่า expect/actual declarations และเป็นแบบทดลอง ใน Kotlin 1.2 มีการเพิ่ม expect-annotation และใน Kotlin 1.3 expect/actual ก็กลายเป็นเสถียรสำหรับคลาสและฟังก์ชัน กลไกได้ถูกขยายเพิ่มขึ้นเรื่อยๆ: ใน Kotlin 1.6 เพิ่มการรองรับ expect/actual สำหรับ companion-object ใน Kotlin 1.7 สำหรับ enum-class และใน Kotlin 2.0 สำหรับ typealias
คุณสมบัติสำคัญของ expect/actual คือ ความปลอดภัยในระดับการคอมไพล์ หากนักพัฒนาเพิ่ม expect-declaration ใน commonMain แต่ลืมจัดเตรียม actual-implementation สำหรับ iOS คอมไพเลอร์จะแจ้งข้อผิดพลาด ซึ่งป้องกัน runtime-failure ที่เกิดขึ้นในแนวทางที่ใช้ reflection หรือการโหลดโค้ดเฉพาะแพลตฟอร์มแบบไดนามิก
กลไก expect/actual ทำงานในระดับ source set — ระบบโมดูลของ Kotlin Multiplatform โค้ดทั่วไปที่สามารถเข้าถึงได้ทุกแพลตฟอร์มจะอยู่ใน source set commonMain โค้ดที่ขึ้นกับแพลตฟอร์มอยู่ใน iosMain, androidMain, macosMain และอื่นๆ คีย์เวิร์ด expect ใน commonMain ประกาศ API ส่วนคีย์เวิร์ด actual ใน source set เฉพาะแพลตฟอร์มจะจัดเตรียม implementation คอมไพเลอร์เชื่อมโยงพวกมันในขั้นตอน code generation โดยแทนที่การเรียกใช้ expect-function ด้วย actual-implementation สำหรับแพลตฟอร์มเป้าหมาย
ลำดับชั้นของ source set ในโปรเจกต์ KMM ทั่วไปมีลักษณะดังนี้: commonMain ประกอบด้วย expect-declaration, iosMain และ androidMain ประกอบด้วย actual-implementation เมื่อคอมไพล์สำหรับ iOS จะใช้ actual จาก iosMain เมื่อคอมไพล์สำหรับ Android จะใช้จาก androidMain Source set อาจมีลำดับชั้นกลาง (เช่น iosArm64Main สำหรับสถาปัตยกรรมเฉพาะ) ซึ่งช่วยให้ปรับแต่ง implementation สำหรับอุปกรณ์ต่างๆ ได้
// commonMain — การประกาศ expect
expect fun getPlatformName(): String
// androidMain — actual สำหรับ Android
actual fun getPlatformName(): String = "Android"
// iosMain — actual สำหรับ iOS
actual fun getPlatformName(): String = "iOS"
Kotlin-compiler ตรวจสอบเงื่อนไขหลายประการเมื่อทำงานกับ expect/actual แต่ละ expect-declaration ต้องมี actual-implementation สำหรับทุกแพลตฟอร์มที่ใช้งานอยู่ ลายเซ็นของ actual-declaration ต้องตรงกับ expect-signature (annotation @OptionalExpectation อาจผ่อนปรนข้อกำหนดนี้) ตัวปรับการเข้าถึง return type และพารามิเตอร์ต้องเหมือนกัน คอมไพเลอร์ยังตรวจสอบว่าไม่มี cyclic dependencies ระหว่าง expect- และ actual-declaration
expect/actual รองรับการประกาศหลายประเภท ที่พบบ่อยที่สุดคือ expect/actual-function สำหรับการดำเนินการเฉพาะแพลตฟอร์ม expect/actual-class สำหรับออบเจ็กต์ที่ต้องการ native implementation และ expect/actual-property สำหรับค่าคงที่และการตั้งค่า แต่ละประเภทมีกฎการใช้งานและข้อจำกัดของตนเอง
Expect/actual-function — เป็นประเภทที่ง่ายและพบบ่อยที่สุด ใช้สำหรับเรียก API ของแพลตฟอร์ม เช่น การรับเวลา การอ่านไฟล์ หรือการส่ง HTTP-request Expect/actual-class ใช้สำหรับสร้างออบเจ็กต์ที่โต้ตอบโดยตรงกับ native code (เช่น การเข้าถึงกล้อง ตำแหน่งที่ตั้ง หรือ key storage) Expect/actual-property (val) เหมาะสำหรับค่าคงที่ของแพลตฟอร์ม — ชื่อ OS เวอร์ชัน SDK หรือพาธไปยังไดเรกทอรีของระบบ
| ประเภทการประกาศ | คีย์เวิร์ด | ตัวอย่างการใช้งาน |
|---|---|---|
| ฟังก์ชัน | expect fun / actual fun | รับค่า unique identifier ของอุปกรณ์ |
| คลาส | expect class / actual class | เข้าถึง SecureStorage (Keychain / EncryptedSharedPreferences) |
| คุณสมบัติ | expect val / actual val | แพลตฟอร์มปัจจุบัน (iOS / Android) |
| Enum-คลาส | expect enum / actual enum | รายการ permission ที่มีในแอปพลิเคชัน |
| Typealias | expect typealias / actual typealias | ประเภทของ network response เฉพาะแพลตฟอร์ม |
ไม่ใช่ทุกโครงสร้างของ Kotlin ที่สามารถใช้กับ expect/actual ได้ Expect-declaration ไม่สามารถมีเนื้อหา — มีเพียงลายเซ็นเท่านั้น Expect-class ไม่สามารถมี constructor พร้อมพารามิเตอร์ (ต้องเป็น primary constructor ว่าง) สำหรับ enum expect/actual ค่าคงที่ทั้งหมดต้องเหมือนกันใน expect และ actual Expect-property ต้องเป็น val (ไม่ใช่ var) เนื่องจากการเก็บสถานะในโมดูลทั่วไปสำหรับคุณสมบัติเฉพาะแพลตฟอร์มนั้นไม่มีความหมาย
มาดูตัวอย่างเชิงปฏิบัติของ expect/actual ตั้งแต่ฟังก์ชันง่ายๆ ไปจนถึงคลาสที่สมบูรณ์ กรณีพื้นฐาน — การรับชื่อของแพลตฟอร์มเพื่อใช้ใน UI ตัวอย่างที่ซับซ้อนขึ้นรวมถึงการเข้าถึง native storage และการทำงานกับ platform threads
// 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-class PlatformStorage กำหนดสัญญาของ storage แบบ key-value อย่างง่าย บน Android implementation ใช้ SharedPreferences ส่วนบน iOS ใช้ Keychain หรือ NSUserDefaults ด้วย expect/actual business logic ใน commonMain สามารถเรียก save/get/remove โดยไม่จำเป็นต้องรู้เกี่ยวกับ implementation ของแพลตฟอร์ม
// 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-declaration ให้มากที่สุด — ยิ่งมีโค้ดทั่วไปมากเท่าไหร่ การบำรุงรักษาก็ยิ่งง่ายขึ้น ใช้ expect/actual เฉพาะกับ API ที่แตกต่างกันในแต่ละแพลตฟอร์มเท่านั้น สำหรับโค้ดที่เหลือ ให้ใช้อินเทอร์เฟซกับ factory หรือ dependency injection ซึ่งช่วยให้การทดสอบง่ายขึ้น
แนะนำให้จัดกลุ่ม expect-declaration ตาม โมดูลตามหัวข้อ แทนที่จะปะปนกันในไฟล์เดียว เช่น Storage.kt สำหรับ expect-declaration ของ storage, Platform.kt สำหรับ expect-function ที่ทำงานกับ OS และ Analytics.kt สำหรับ expect-class ด้าน analytics ช่วยให้การนำทางและทำความเข้าใจพื้นผิวของแพลตฟอร์มในโปรเจกต์ KMM ง่ายขึ้น แต่ละ actual-file ควรอยู่ใน source set ที่เกี่ยวข้อง: androidMain, iosMain, desktopMain และอื่นๆ
Default-implementation ผ่าน expect fun กับ actual fun โดยที่ actual ใช้ common-code — เป็น anti-pattern ที่พบบ่อย หาก implementation ของแพลตฟอร์มไม่แตกต่างจาก default ก็ไม่จำเป็นต้องใช้ expect/actual ในกรณีเช่นนี้ ให้ใช้ฟังก์ชันธรรมดาใน commonMain นอกจากนี้ หลีกเลี่ยง expect/actual สำหรับ getter ธรรมดา — ให้ใช้ expect val กับค่าคงที่
โครงสร้างที่ถูกต้องของโค้ด expect/actual มีความสำคัญต่อการอ่านโปรเจกต์ แต่ละโมดูล expect/actual ควรมีจุดเริ่มต้นเดียว ตัวอย่างการจัดระเบียบ: commonMain/kotlin/com/project/platform ประกอบด้วย expect-declaration, androidMain/kotlin/com/project/platform — actual สำหรับ Android, iosMain/kotlin/com/project/platform — actual สำหรับ iOS ชื่อไฟล์และแพ็กเกจควรตรงกันสำหรับ expect และ actual เพื่อให้นักพัฒนาสามารถค้นหา implementation ที่สอดคล้องกันได้อย่างรวดเร็ว
อินเทอร์เฟซ กับ platform factory — เป็นทางเลือกหลักของ expect/actual แทนที่จะใช้ expect-class สามารถประกาศอินเทอร์เฟซใน commonMain และสร้าง concrete class ในโมดูลเฉพาะแพลตฟอร์ม Factory หรือ dependency injection container จัดเตรียม implementation ที่ถูกต้องใน runtime แนวทางนี้เหมาะสำหรับการทดสอบมากกว่า เนื่องจากอินเทอร์เฟซสามารถ mock ได้
Dependency Injection (Koin, Kodein) — เป็นแนวทางที่ยืดหยุ่นกว่าแต่มีประสิทธิภาพน้อยกว่า DI-container ถูกกำหนดแยกกันสำหรับแต่ละแพลตฟอร์มและจัดเตรียม dependencies เฉพาะแพลตฟอร์มให้กับ common-code ต่างจาก expect/actual การ injection เกิดขึ้นใน runtime ซึ่งทำให้สามารถสลับ implementation สำหรับการทดสอบได้ ในทางกลับกัน ข้อผิดพลาดในการกำหนดค่า DI จะถูกตรวจพบเมื่อรันเท่านั้น ไม่ใช่ในขั้นตอนการคอมไพล์
| แนวทาง | การตรวจสอบในขั้นตอนการคอมไพล์ | ความยืดหยุ่นในการทดสอบ | ค่าใช้จ่ายใน Runtime |
|---|---|---|---|
| expect/actual | สมบูรณ์ | ต่ำ (actual ไม่สามารถ mock ได้) | เป็นศูนย์ (การเชื่อมโยงตอนคอมไพล์) |
| อินเทอร์เฟซ + factory | บางส่วน | สูง (สามารถ mock ได้) | น้อยที่สุด (virtual call) |
| Dependency Injection | ไม่มี (runtime) | สูง | ปานกลาง (DI-proxy) |
การเลือกระหว่าง expect/actual กับทางเลือกอื่นขึ้นอยู่กับบริบท สำหรับ ประสิทธิภาพที่สำคัญ (game engine, real-time processing) expect/actual เหมาะสมกว่าเนื่องจากไม่มีค่าใช้จ่ายเพิ่มเติม สำหรับ business logic (repository, use-case) ควรใช้อินเทอร์เฟซกับ DI เพื่อให้การทดสอบง่ายขึ้น แนวทางแบบผสม — expect/actual สำหรับการดำเนินการเฉพาะแพลตฟอร์มระดับต่ำและอินเทอร์เฟซสำหรับชั้น business logic — ถูกใช้ในโปรเจกต์ KMM ระดับ production ส่วนใหญ่
คำถามที่พบบ่อย
expect/actual เชื่อมโยง implementation ในขั้นตอนการคอมไพล์โดยไม่มี virtual call ส่วนอินเทอร์เฟซทำใน runtime expect/actual รับประกันว่า implementation มีอยู่สำหรับทุกแพลตฟอร์ม ส่วนอินเทอร์เฟซต้องการการตรวจสอบใน runtime
ได้ expect enum รองรับตั้งแต่ Kotlin 1.7 เป็นต้นไป ค่าคงที่ทั้งหมดใน expect และ actual enum ต้องตรงกัน ค่าคงที่ที่แตกต่างกันในแต่ละแพลตฟอร์มเป็นข้อผิดพลาดในการคอมไพล์
คอมไพเลอร์ จะแจ้งข้อผิดพลาดสำหรับแต่ละแพลตฟอร์มที่ไม่มี actual-implementation โปรเจกต์จะไม่สามารถ build ได้จนกว่า expect-declaration ทุกตัวจะมี actual-implementation ที่สอดคล้องกัน
ไม่ได้ expect และ actual ต้องอยู่ใน source set ที่ต่างกัน expect — ใน commonMain หรือ source set ระดับกลาง actual — ใน source set เฉพาะแพลตฟอร์ม การวาง expect และ actual ใน source set เดียวกันเป็นข้อผิดพลาดในการคอมไพล์
สำหรับการทดสอบ expect/actual ให้ใช้ commonTest กับ source set สำหรับทดสอบเฉพาะแพลตฟอร์ม เขียน expect-test ใน commonTest และ actual-test สำหรับแต่ละแพลตฟอร์ม Integration test จะรันแยกกันในแต่ละแพลตฟอร์มเป้าหมาย
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ