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?
Phần tiêu đề “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
Phần tiêu đề “Cú pháp cơ bản”expect (trong commonMain)
Phần tiêu đề “expect (trong commonMain)”// Khai báo: "Tôi cần một function/class này"expect fun platformName(): Stringactual (trong androidMain)
Phần tiêu đề “actual (trong androidMain)”// Android implementationactual fun platformName(): String = "Android"actual (trong iosMain)
Phần tiêu đề “actual (trong iosMain)”// iOS implementationactual fun platformName(): String = "iOS"Ví dụ 1: Function đơn giản
Phần tiêu đề “Ví dụ 1: Function đơn giản”commonMain/Platform.kt
Phần tiêu đề “commonMain/Platform.kt”expect fun getCurrentTimeMillis(): LongandroidMain/Platform.android.kt
Phần tiêu đề “androidMain/Platform.android.kt”actual fun getCurrentTimeMillis(): Long = System.currentTimeMillis()iosMain/Platform.ios.kt
Phần tiêu đề “iosMain/Platform.ios.kt”import platform.Foundation.NSDateimport platform.Foundation.timeIntervalSince1970
actual fun getCurrentTimeMillis(): Long = (NSDate().timeIntervalSince1970 * 1000).toLong()Sử dụng (trong commonMain)
Phần tiêu đề “Sử dụng (trong commonMain)”fun logTimestamp() { val time = getCurrentTimeMillis() println("Current time: $time")}Ví dụ 2: Class với constructor
Phần tiêu đề “Ví dụ 2: Class với constructor”commonMain/Platform.kt
Phần tiêu đề “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
Phần tiêu đề “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
Phần tiêu đề “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
Phần tiêu đề “Ví dụ 3: Class với parameters”commonMain/FileStorage.kt
Phần tiêu đề “commonMain/FileStorage.kt”expect class FileStorage(basePath: String) { fun readFile(fileName: String): String? fun writeFile(fileName: String, content: String)}androidMain/FileStorage.android.kt
Phần tiêu đề “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
Phần tiêu đề “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)
Phần tiêu đề “Ví dụ 4: Object (Singleton)”commonMain/Logger.kt
Phần tiêu đề “commonMain/Logger.kt”expect object Logger { fun debug(tag: String, message: String) fun error(tag: String, message: String)}androidMain/Logger.android.kt
Phần tiêu đề “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
Phần tiêu đề “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
Phần tiêu đề “Ví dụ 5: UUID Generator”commonMain/UuidGenerator.kt
Phần tiêu đề “commonMain/UuidGenerator.kt”expect fun randomUUID(): StringandroidMain/UuidGenerator.android.kt
Phần tiêu đề “androidMain/UuidGenerator.android.kt”import java.util.UUID
actual fun randomUUID(): String = UUID.randomUUID().toString()iosMain/UuidGenerator.ios.kt
Phần tiêu đề “iosMain/UuidGenerator.ios.kt”import platform.Foundation.NSUUID
actual fun randomUUID(): String = NSUUID().UUIDString()Pattern nâng cao: Interface + Factory
Phần tiêu đề “Pattern nâng cao: Interface + Factory”Thay vì expect class trực tiếp, dùng interface + factory:
commonMain
Phần tiêu đề “commonMain”// Interface chunginterface HttpClient { suspend fun get(url: String): String}
// Factory functionexpect fun createHttpClient(): HttpClientandroidMain
Phần tiêu đề “androidMain”import okhttp3.OkHttpClientimport 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
Phần tiêu đề “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
Phần tiêu đề “Quy tắc quan trọng”✅ Được phép
Phần tiêu đề “✅ Được phép”// expect functionexpect fun doSomething(): String
// expect class với constructorexpect class MyClass() { fun method(): Int}
// expect objectexpect object MySingleton { fun action()}
// expect với default parametersexpect fun greet(name: String = "World"): String❌ Không được phép
Phần tiêu đề “❌ 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 signatureexpect fun process(input: String): Intactual fun process(input: String, extra: Boolean): Int // ❌ Khác signature📝 Tóm tắt
Phần tiêu đề “📝 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
Phần tiêu đề “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
Phần tiêu đề “Tiếp theo”Học cách dùng Ktor để làm networking - thư viện HTTP multiplatform.