Skip to Content

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ăngAndroidiOS
Lưu fileContext.filesDirNSFileManager
HTTP clientOkHttpDarwin/URLSession
UUIDjava.util.UUIDNSUUID
Current timeSystem.currentTimeMillis()NSDate
Device infoBuild.VERSIONUIDevice

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(): String

actual (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(): Long

androidMain/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(): String

androidMain/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(): HttpClient

androidMain

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

KeywordVị tríMục đích
expectcommonMainKhai báo cần implementation
actualandroidMain/iosMainCung cấp implementation

Best Practices

  1. Chỉ dùng expect/actual khi thật sự cần platform-specific code
  2. Ưu tiên dùng thư viện multiplatform có sẵn (Ktor, SQLDelight, KotlinX…)
  3. Giữ expect/actual đơn giản - logic phức tạp nên ở commonMain
  4. 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