Skip to content

平台特定代码组织

在 Kotlin Multiplatform 项目中,合理组织平台特定代码是保持项目可维护性的关键。本文介绍如何在最大化代码共享的同时,优雅地处理平台差异。

代码组织策略

策略对比

策略适用场景优点缺点
expect/actual访问平台 API编译时保证,零运行时开销每个平台必须实现
依赖注入可替换的业务逻辑灵活,便于测试,解耦需要 DI 框架,运行时开销
接口 + 平台实现复杂平台特性清晰的抽象边界需手动注入实例
typealias平台类型映射简单直接仅适用于类型别名

模式 1:Platform API 抽象层

标准代码块

kotlin
// 定义平台能力接口
interface PlatformLogger {
    fun log(message: String)
    fun error(message: String, throwable: Throwable?)
}

interface PlatformStorage {
    suspend fun save(key: String, value: String)
    suspend fun load(key: String): String?
    suspend fun remove(key: String)
}

// 业务层依赖接口
class UserRepository(
    private val storage: PlatformStorage,
    private val logger: PlatformLogger
) {
    suspend fun saveUser(user: User) {
        logger.log("Saving user: ${user.id}")
        storage.save("user_${user.id}", user.toJson())
    }
    
    suspend fun getUser(id: String): User? {
        return try {
            val json = storage.load("user_$id")
            json?.let { User.fromJson(it) }
        } catch (e: Exception) {
            logger.error("Failed to load user", e)
            null
        }
    }
}
kotlin
import android.content.Context
import android.content.SharedPreferences
import android.util.Log

class AndroidLogger : PlatformLogger {
    override fun log(message: String) {
        Log.d("KMP", message)
    }
    
    override fun error(message: String, throwable: Throwable?) {
        Log.e("KMP", message, throwable)
    }
}

class AndroidStorage(context: Context) : PlatformStorage {
    private val prefs: SharedPreferences = context.getSharedPreferences(
        "kmp_storage",
        Context.MODE_PRIVATE
    )
    
    override suspend fun save(key: String, value: String) {
        prefs.edit().putString(key, value).apply()
    }
    
    override suspend fun load(key: String): String? {
        return prefs.getString(key, null)
    }
    
    override suspend fun remove(key: String) {
        prefs.edit().remove(key).apply()
    }
}
kotlin
import platform.Foundation.*

class IosLogger : PlatformLogger {
    override fun log(message: String) {
        NSLog("[KMP] %@", message)
    }
    
    override fun error(message: String, throwable: Throwable?) {
        NSLog("[KMP ERROR] %@: %@", message, throwable?.message ?: "")
    }
}

class IosStorage : PlatformStorage {
    private val userDefaults = NSUserDefaults.standardUserDefaults
    
    override suspend fun save(key: String, value: String) {
        userDefaults.setObject(value, forKey = key)
    }
    
    override suspend fun load(key: String): String? {
        return userDefaults.stringForKey(key)
    }
    
    override suspend fun remove(key: String) {
        userDefaults.removeObjectForKey(key)
    }
}

使用 expect/actual 提供工厂函数

kotlin
// 使用 expect 函数创建平台实例
expect fun createPlatformLogger(): PlatformLogger
expect fun createPlatformStorage(): PlatformStorage

// 或者使用 expect 对象
expect object Platform {
    fun createLogger(): PlatformLogger
    fun createStorage(): PlatformStorage
}
kotlin
import android.content.Context

// 需要 Context 的情况
actual fun createPlatformLogger(): PlatformLogger = AndroidLogger()

// 保存 Context 引用
object AndroidContext {
    lateinit var context: Context
}

actual fun createPlatformStorage(): PlatformStorage = 
    AndroidStorage(AndroidContext.context)

// 或者
actual object Platform {
    actual fun createLogger(): PlatformLogger = AndroidLogger()
    actual fun createStorage(): PlatformStorage = 
        AndroidStorage(AndroidContext.context)
}
kotlin
actual fun createPlatformLogger(): PlatformLogger = IosLogger()
actual fun createPlatformStorage(): PlatformStorage = IosStorage()

// 或者
actual object Platform {
    actual fun createLogger(): PlatformLogger = IosLogger()
    actual fun createStorage(): PlatformStorage = IosStorage()
}

模式 2:分层架构设计

清晰的职责分离

                    ┌─────────────────┐
                    │  App/UI Layer   │ (平台特定)
                    └────────┬────────┘

                    ┌────────▼────────┐
                    │ Presentation    │ (共享)
                    │   (ViewModel)   │
                    └────────┬────────┘

                    ┌────────▼────────┐
                    │  Domain Layer   │ (共享)
                    │  (Use Cases)    │
                    └────────┬────────┘

                    ┌────────▼────────┐
                    │   Data Layer    │ (共享接口)
                    │  (Repository)   │
                    └────────┬────────┘

           ┌─────────────────┼─────────────────┐
           ▼                 ▼                 ▼
    ┌──────────┐      ┌──────────┐     ┌──────────┐
    │ Android  │      │   iOS    │     │Desktop/  │
    │   Impl   │      │   Impl   │     │Web Impl  │
    └──────────┘      └──────────┘     └──────────┘

标准代码块

kotlin
// Data Layer - 仓库接口
interface UserRepository {
    suspend fun getUser(id: String): Result<User>
    suspend fun saveUser(user: User): Result<Unit>
}

// Data Layer - 数据源抽象
interface RemoteDataSource {
    suspend fun fetchUser(id: String): User
}

interface LocalDataSource {
    suspend fun saveUser(user: User)
    suspend fun getUser(id: String): User?
}

// 共享的仓库实现
class UserRepositoryImpl(
    private val remote: RemoteDataSource,
    private val local: LocalDataSource
) : UserRepository {
    override suspend fun getUser(id: String): Result<User> = runCatching {
        // 先从本地获取
        local.getUser(id) ?: run {
            // 本地没有,从远程获取并缓存
            val user = remote.fetchUser(id)
            local.saveUser(user)
            user
        }
    }
    
    override suspend fun saveUser(user: User): Result<Unit> = runCatching {
        local.saveUser(user)
        // 可选:同步到远程
    }
}
kotlin
// Domain Layer - 用例
class GetUserUseCase(private val repository: UserRepository) {
    suspend operator fun invoke(userId: String): Result<User> {
        return repository.getUser(userId)
    }
}
kotlin
import android.content.Context
import io.ktor.client.*

class AndroidRemoteDataSource(
    private val httpClient: HttpClient
) : RemoteDataSource {
    override suspend fun fetchUser(id: String): User {
        // 使用 Ktor 实现网络请求
        TODO()
    }
}

class AndroidLocalDataSource(
    context: Context
) : LocalDataSource {
    // 使用 Room 或 SQLDelight
    override suspend fun saveUser(user: User) {
        TODO()
    }
    
    override suspend fun getUser(id: String): User? {
        TODO()
    }
}

模式 3:Context 传递策略

Android Context 处理

方式 1:全局 Context 持有者

kotlin
// androidMain
object AndroidContext {
    lateinit var app: Application
    
    fun init(application: Application) {
        app = application
    }
}

// 在 Application 中初始化
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        AndroidContext.init(this)
    }
}

方式 2:通过构造函数传递

kotlin
// androidMain
actual fun createUserRepository(context: Any?): UserRepository {
    require(context is Context) { "Android requires Context" }
    return UserRepositoryImpl(
        remote = AndroidRemoteDataSource(),
        local = AndroidLocalDataSource(context)
    )
}

// iosMain
actual fun createUserRepository(context: Any?): UserRepository {
    // iOS 不需要 Context
    return UserRepositoryImpl(
        remote = IosRemoteDataSource(),
        local = IosLocalDataSource()
    )
}

方式 3:使用依赖注入

kotlin
// commonMain
val commonModule = module {
    single<UserRepository> { UserRepositoryImpl(get(), get()) }
    single { GetUserUseCase(get()) }
}

// androidMain
val androidModule = module {
    single<RemoteDataSource> { AndroidRemoteDataSource(get()) }
    single<LocalDataSource> { AndroidLocalDataSource(get()) }
}

// iosMain
val iosModule = module {
    single<RemoteDataSource> { IosRemoteDataSource() }
    single<LocalDataSource> { IosLocalDataSource() }
}

模式 4:平台能力检测

特性可用性检查

kotlin
// commonMain
interface PlatformCapabilities {
    val supportsBiometric: Boolean
    val supportsCamera: Boolean
    val supportsNFC: Boolean
}

expect fun getPlatformCapabilities(): PlatformCapabilities

// 业务逻辑中使用
class AuthManager(private val capabilities: PlatformCapabilities) {
    fun canUseBiometric(): Boolean = capabilities.supportsBiometric
}
kotlin
// androidMain
import android.content.Context
import android.content.pm.PackageManager
import androidx.biometric.BiometricManager

class AndroidCapabilities(private val context: Context) : PlatformCapabilities {
    override val supportsBiometric: Boolean
        get() = BiometricManager.from(context)
            .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) ==
            BiometricManager.BIOMETRIC_SUCCESS
    
    override val supportsCamera: Boolean
        get() = context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
    
    override val supportsNFC: Boolean
        get() = context.packageManager.hasSystemFeature(PackageManager.FEATURE_NFC)
}

actual fun getPlatformCapabilities(): PlatformCapabilities = 
    AndroidCapabilities(AndroidContext.app)
kotlin
// iosMain
import platform.LocalAuthentication.LAContext
import platform.AVFoundation.AVCaptureDevice
import platform.CoreNFC.NFCNDEFReaderSession

class IosCapabilities : PlatformCapabilities {
    override val supportsBiometric: Boolean
        get() = LAContext().canEvaluatePolicy(/* ... */)
    
    override val supportsCamera: Boolean
        get() = AVCaptureDevice.devicesWithMediaType(/* ... */).size > 0
    
    override val supportsNFC: Boolean
        get() = NFCNDEFReaderSession.readingAvailable
}

actual fun getPlatformCapabilities(): PlatformCapabilities = IosCapabilities()

最佳实践

✅ 实践 1:最小化平台代码

kotlin
// ✅ 推荐:大部分逻辑在 commonMain
class UserViewModel(private val repository: UserRepository) {
    val userState = MutableStateFlow<User?>(null)
    
    fun loadUser(id: String) {
        viewModelScope.launch {
            userState.value = repository.getUser(id).getOrNull()
        }
    }
}

// 平台层只负责提供依赖
// androidMain
fun createUserViewModel() = UserViewModel(
    repository = createUserRepository(context = AndroidContext.app)
)

✅ 实践 2:使用接口而非 expect class

kotlin
// ✅ 推荐:使用接口
interface Logger {
    fun log(message: String)
}

expect fun createLogger(): Logger

// ❌ 不推荐:过度使用 expect class
expect class Logger {
    fun log(message: String)
}

✅ 实践 3:清晰的抽象边界

kotlin
// ✅ 每个平台能力有清晰的接口
interface FileSystem { }
interface NetworkClient { }
interface Database { }

// ❌ 避免过于泛化的抽象
interface Platform {
    fun doEverything(): Any
}

✅ 实践 4:使用 typealias 映射平台类型

kotlin
// commonMain
expect class UUID

fun generateId(): UUID = createUUID()
expect fun createUUID(): UUID

// androidMain
actual typealias UUID = java.util.UUID
actual fun createUUID(): UUID = UUID.randomUUID()

// iosMain
import platform.Foundation.NSUUID
actual typealias UUID = NSUUID
actual fun createUUID(): UUID = NSUUID()

❌ 避免平台泄漏

kotlin
// ❌ 不要在共享代码中暴露平台类型
// commonMain
class DataManager {
    fun getContext(): android.content.Context // ❌ 编译错误
}

// ✅ 使用泛型或 Any 类型
class DataManager {
    fun getPlatformContext(): Any? // ✅
}

代码组织目录结构建议

shared/src/
├── commonMain/kotlin/com/example/
│   ├── data/
│   │   ├── repository/
│   │   │   ├── UserRepositoryImpl.kt (共享实现)
│   │   │   └── UserRepository.kt (接口)
│   │   ├── source/
│   │   │   ├── remote/RemoteDataSource.kt (接口)
│   │   │   └── local/LocalDataSource.kt (接口)
│   │   └── model/
│   │       └── User.kt
│   ├── domain/
│   │   └── usecase/
│   │       └── GetUserUseCase.kt
│   └── platform/
│       ├── PlatformLogger.kt (接口)
│       ├── PlatformStorage.kt (接口)
│       └── Platform.kt (expect 声明)

├── androidMain/kotlin/com/example/
│   ├── data/
│   │   └── source/
│   │       ├── AndroidRemoteDataSource.kt (Android 实现)
│   │       └── AndroidLocalDataSource.kt (Android 实现)
│   └── platform/
│       ├── AndroidLogger.kt
│       ├── AndroidStorage.kt
│       └── Platform.kt (actual 实现)

└── iosMain/kotlin/com/example/
    ├── data/
    │   └── source/
    │       ├── IosRemoteDataSource.kt (iOS 实现)
    │       └── IosLocalDataSource.kt (iOS 实现)
    └── platform/
        ├── IosLogger.kt
        ├── IosStorage.kt
        └── Platform.kt (actual 实现)

通过合理组织平台特定代码,可以在保持共享代码最大化的同时,充分利用各平台的原生能力,构建可维护、可扩展的跨平台应用。