Bỏ qua để đến nội dung

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.

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).


// Khai báo: "Tôi cần một function/class này"
expect fun platformName(): String
// Android implementation
actual fun platformName(): String = "Android"
// iOS implementation
actual fun platformName(): String = "iOS"

expect fun getCurrentTimeMillis(): Long
actual fun getCurrentTimeMillis(): Long = System.currentTimeMillis()
import platform.Foundation.NSDate
import platform.Foundation.timeIntervalSince1970
actual fun getCurrentTimeMillis(): Long =
(NSDate().timeIntervalSince1970 * 1000).toLong()
fun logTimestamp() {
val time = getCurrentTimeMillis()
println("Current time: $time")
}

expect class Platform() {
val name: String
val version: String
}

Lưu ý: actual constructor() phải match với expect.

import android.os.Build
actual class Platform actual constructor() {
actual val name: String = "Android"
actual val version: String = Build.VERSION.SDK_INT.toString()
}
import platform.UIKit.UIDevice
actual class Platform actual constructor() {
actual val name: String = UIDevice.currentDevice.systemName()
actual val version: String = UIDevice.currentDevice.systemVersion
}

expect class FileStorage(basePath: String) {
fun readFile(fileName: String): String?
fun writeFile(fileName: String, content: String)
}
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)
}
}
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
)
}
}

expect object Logger {
fun debug(tag: String, message: String)
fun error(tag: String, message: String)
}
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)
}
}
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")
}
}

expect fun randomUUID(): String
import java.util.UUID
actual fun randomUUID(): String = UUID.randomUUID().toString()
import platform.Foundation.NSUUID
actual fun randomUUID(): String = NSUUID().UUIDString()

Thay vì expect class trực tiếp, dùng interface + factory:

// Interface chung
interface HttpClient {
suspend fun get(url: String): String
}
// Factory function
expect fun createHttpClient(): HttpClient
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()
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

// 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
// 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

Keyword Vị trí Mục đích
expect commonMain Khai báo cần implementation
actual androidMain/iosMain Cung cấp implementation
  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

Học cách dùng Ktor để làm networking - thư viện HTTP multiplatform.