Expect/Actual Pattern
Pattern expect/actual là cách KMP cho phép bạn viết code khác nhau cho từng platform trong khi vẫn giữ API chung.
Tại sao cần expect/actual?
Một số thứ không thể chia sẻ giữa các platforms:
| Tính năng | Android | iOS |
|---|---|---|
| Lưu file | Context.filesDir | NSFileManager |
| HTTP client | OkHttp | Darwin/URLSession |
| UUID | java.util.UUID | NSUUID |
| Current time | System.currentTimeMillis() | NSDate |
| Device info | Build.VERSION | UIDevice |
Giải pháp: Định nghĩa interface chung (expect), mỗi platform tự implement (actual).
Cú pháp cơ bản
expect (trong commonMain)
// Khai báo: "Tôi cần một function/class này"
expect fun platformName(): Stringactual (trong androidMain)
// Android implementation
actual fun platformName(): String = "Android"actual (trong iosMain)
// iOS implementation
actual fun platformName(): String = "iOS"Ví dụ 1: Function đơn giản
commonMain/Platform.kt
expect fun getCurrentTimeMillis(): LongandroidMain/Platform.android.kt
actual fun getCurrentTimeMillis(): Long = System.currentTimeMillis()iosMain/Platform.ios.kt
import platform.Foundation.NSDate
import platform.Foundation.timeIntervalSince1970
actual fun getCurrentTimeMillis(): Long =
(NSDate().timeIntervalSince1970 * 1000).toLong()Sử dụng (trong commonMain)
fun logTimestamp() {
val time = getCurrentTimeMillis()
println("Current time: $time")
}Ví dụ 2: Class với constructor
commonMain/Platform.kt
expect class Platform() {
val name: String
val version: String
}Lưu ý: actual constructor() phải match với expect.
androidMain/Platform.android.kt
import android.os.Build
actual class Platform actual constructor() {
actual val name: String = "Android"
actual val version: String = Build.VERSION.SDK_INT.toString()
}iosMain/Platform.ios.kt
import platform.UIKit.UIDevice
actual class Platform actual constructor() {
actual val name: String = UIDevice.currentDevice.systemName()
actual val version: String = UIDevice.currentDevice.systemVersion
}Ví dụ 3: Class với parameters
commonMain/FileStorage.kt
expect class FileStorage(basePath: String) {
fun readFile(fileName: String): String?
fun writeFile(fileName: String, content: String)
}androidMain/FileStorage.android.kt
import java.io.File
actual class FileStorage actual constructor(private val basePath: String) {
actual fun readFile(fileName: String): String? {
val file = File(basePath, fileName)
return if (file.exists()) file.readText() else null
}
actual fun writeFile(fileName: String, content: String) {
File(basePath, fileName).writeText(content)
}
}iosMain/FileStorage.ios.kt
import platform.Foundation.*
actual class FileStorage actual constructor(private val basePath: String) {
actual fun readFile(fileName: String): String? {
val path = "$basePath/$fileName"
return NSString.stringWithContentsOfFile(
path,
encoding = NSUTF8StringEncoding,
error = null
)
}
actual fun writeFile(fileName: String, content: String) {
val path = "$basePath/$fileName"
(content as NSString).writeToFile(
path,
atomically = true,
encoding = NSUTF8StringEncoding,
error = null
)
}
}Ví dụ 4: Object (Singleton)
commonMain/Logger.kt
expect object Logger {
fun debug(tag: String, message: String)
fun error(tag: String, message: String)
}androidMain/Logger.android.kt
import android.util.Log
actual object Logger {
actual fun debug(tag: String, message: String) {
Log.d(tag, message)
}
actual fun error(tag: String, message: String) {
Log.e(tag, message)
}
}iosMain/Logger.ios.kt
import platform.Foundation.NSLog
actual object Logger {
actual fun debug(tag: String, message: String) {
NSLog("[$tag] DEBUG: $message")
}
actual fun error(tag: String, message: String) {
NSLog("[$tag] ERROR: $message")
}
}Ví dụ 5: UUID Generator
commonMain/UuidGenerator.kt
expect fun randomUUID(): StringandroidMain/UuidGenerator.android.kt
import java.util.UUID
actual fun randomUUID(): String = UUID.randomUUID().toString()iosMain/UuidGenerator.ios.kt
import platform.Foundation.NSUUID
actual fun randomUUID(): String = NSUUID().UUIDString()Pattern nâng cao: Interface + Factory
Thay vì expect class trực tiếp, dùng interface + factory:
commonMain
// Interface chung
interface HttpClient {
suspend fun get(url: String): String
}
// Factory function
expect fun createHttpClient(): HttpClientandroidMain
import okhttp3.OkHttpClient
import okhttp3.Request
class AndroidHttpClient : HttpClient {
private val client = OkHttpClient()
override suspend fun get(url: String): String {
val request = Request.Builder().url(url).build()
return client.newCall(request).execute().body?.string() ?: ""
}
}
actual fun createHttpClient(): HttpClient = AndroidHttpClient()iosMain
import platform.Foundation.*
class IosHttpClient : HttpClient {
override suspend fun get(url: String): String {
// iOS implementation with NSURLSession
// ...
}
}
actual fun createHttpClient(): HttpClient = IosHttpClient()Lợi ích:
- Interface có thể mock trong tests
- Dễ thay đổi implementation
- Clean architecture friendly
Quy tắc quan trọng
✅ Được phép
// expect function
expect fun doSomething(): String
// expect class với constructor
expect class MyClass() {
fun method(): Int
}
// expect object
expect object MySingleton {
fun action()
}
// expect với default parameters
expect fun greet(name: String = "World"): String❌ Không được phép
// expect property ở top-level (phải trong class)
expect val myProperty: String // ❌
// expect interface (không cần vì interface đã là abstract)
expect interface MyInterface // ❌ Không cần expect
// actual với khác signature
expect fun process(input: String): Int
actual fun process(input: String, extra: Boolean): Int // ❌ Khác signature📝 Tóm tắt
| Keyword | Vị trí | Mục đích |
|---|---|---|
expect | commonMain | Khai báo cần implementation |
actual | androidMain/iosMain | Cung cấp implementation |
Best Practices
- Chỉ dùng expect/actual khi thật sự cần platform-specific code
- Ưu tiên dùng thư viện multiplatform có sẵn (Ktor, SQLDelight, KotlinX…)
- Giữ expect/actual đơn giản - logic phức tạp nên ở commonMain
- Dùng interface + factory cho code testable hơn
Tiếp theo
Học cách dùng Ktor để làm networking - thư viện HTTP multiplatform.
Last updated on