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