KMP实战:从单端到跨平台的完整迁移指南

KMP跨平台开发实战:从Android单端到多平台适配

本文基于实际项目经验,详细介绍Kotlin Multiplatform (KMP) 的接入方式、平台适配方案以及开发中遇到的技术问题与解决方案。

引言

在移动应用开发中,往往需要同时维护Android和iOS两个平台的应用。传统方案需要分别用Kotlin和Swift开发,导致业务逻辑重复、维护成本高。KMP (Kotlin Multiplatform) 提供了共享业务逻辑和UI代码的解决方案。

经过实际项目验证,采用KMP后可以实现约70-80%的代码复用,显著降低开发和维护成本。

项目创建方式

方式1:使用Android Studio模板创建(推荐)

创建步骤:

  1. 打开 File → New → New Project
  2. 选择 Kotlin Multiplatform Mobile
  3. 填写项目信息:
    • Project Name: AnimationDemo
    • Package: com.yourcompany.anim
    • Minimum SDK: API 24
    • iOS Deployment Target: 13.0
  4. 选择模板:Empty Compose Multiplatform App

生成的项目结构:

bash 复制代码
AnimationDemo/
├── gradle/
│   └── libs.versions.toml      # 版本管理
├── shared/
│   ├── build.gradle.kts
│   └── src/
│       ├── commonMain/         # 共享代码
│       │   └── kotlin/
│       │       └── App.kt
│       ├── androidMain/        # Android特定代码
│       └── iosMain/            # iOS特定代码
├── androidApp/
│   └── src/main/java/
│       └── MainActivity.kt
└── iosApp/
    ├── iosApp.xcodeproj/
    └── iosApp/
        └── ContentView.swift

自动生成的关键配置:

kotlin 复制代码
// shared/build.gradle.kts
plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.compose.compiler)
}

kotlin {
    androidTarget {
        compilations.all {
            kotlinOptions {
                jvmTarget = "11"
            }
        }
    }
    
    listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "shared"
            isStatic = true
        }
    }
    
    sourceSets {
        commonMain.dependencies {
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material3)
            implementation(compose.ui)
            implementation(libs.kotlinx.coroutines.core)
        }
        
        androidMain.dependencies {
            implementation(libs.androidx.activity.compose)
        }
    }
}

优势:

  • 自动配置所有必需的插件
  • 项目结构符合KMP最佳实践
  • 减少配置错误
  • 节省项目初始化时间

方式2:改造现有Android项目

如果已有Android Compose项目需要迁移,需要手动改造:

步骤1:添加KMP插件
kotlin 复制代码
// build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.multiplatform") version "1.9.20" apply false
    id("org.jetbrains.compose") version "1.5.4" apply false
}
步骤2:创建shared模块结构

手动创建目录结构:

bash 复制代码
mkdir -p shared/src/commonMain/kotlin
mkdir -p shared/src/androidMain/kotlin
mkdir -p shared/src/iosMain/kotlin

创建shared/build.gradle.kts配置文件。

步骤3:迁移业务代码

原始结构:

css 复制代码
app/src/main/java/com/yourcompany/
├── ui/
│   ├── HomePage.kt
│   └── ProfilePage.kt
├── network/
│   └── ApiClient.kt
└── MainActivity.kt

迁移后的结构:

bash 复制代码
shared/src/commonMain/kotlin/com/yourcompany/
├── ui/
│   ├── HomePage.kt          # 迁移
│   └── ProfilePage.kt       # 迁移
├── network/
│   └── ApiClient.kt         # 迁移
└── App.kt                    # 新建入口

app/src/main/java/com/yourcompany/
└── MainActivity.kt          # 保留,引用shared
步骤4:修改app模块依赖
kotlin 复制代码
// app/build.gradle.kts
dependencies {
    implementation(project(":shared"))
}
步骤5:创建iOS入口
kotlin 复制代码
// shared/src/iosMain/kotlin/com/yourcompany/MainViewController.kt
package com.yourcompany

import androidx.compose.ui.window.ComposeUIViewController
import com.yourcompany.App

fun MainViewController() = ComposeUIViewController { 
    App() 
}

两种方式对比

维度 新建KMP项目 改造现有项目
初始化时间 5分钟 1-2小时
配置复杂度
迁移工作量 需移动代码和重构
项目结构 自动规范 需手动调整
适用场景 新项目 已有Android项目
推荐度 比较推荐 非必须的话不推荐

建议: 项目页面少于10个优先选择新建;大型项目建议分模块渐进式迁移。

Android端接入

依赖配置

在Android模块的build.gradle.kts中添加对shared模块的依赖:

kotlin 复制代码
dependencies {
    implementation(project(":shared"))
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.activity.compose)
}

使用shared代码

MainActivity通过setContent直接使用shared模块的组件:

kotlin 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setContent {
            MyAppTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    App()  // 来自shared模块
                }
            }
        }
    }
}

Android平台特定处理

状态栏配置

透明状态栏等Android特有的UI配置需要在MainActivity中处理:

kotlin 复制代码
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    window.statusBarColor = Color.TRANSPARENT
    window.setFlags(
        WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
        WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
    )
    
    WindowCompat.getInsetsController(window, window.decorView)
        .isAppearanceLightStatusBars = true
    
    setContent {
        App()
    }
}
权限请求

使用expect/actual模式处理平台差异:

kotlin 复制代码
// commonMain
expect fun requestPermission(permission: String): Boolean

// androidMain
actual fun requestPermission(permission: String): Boolean {
    return ContextCompat.checkSelfPermission(context, permission) == 
           PackageManager.PERMISSION_GRANTED
}

iOS端接入

Framework编译

每次修改shared模块后需要重新编译iOS Framework:

bash 复制代码
./gradlew :shared:linkDebugFrameworkIosArm64

编译产物位于:shared/build/xcode-frameworks/

Xcode集成

iOS端通过SwiftUI包装Compose组件:

swift 复制代码
import SwiftUI
import shared  // Framework名称

struct ContentView: View {
    var body: some View {
        ComposeView()
            .ignoresSafeArea(.keyboard)
    }
}

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        MainViewControllerKt.MainViewController()
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        // 通常为空
    }
}

iOS入口实现

kotlin 复制代码
// shared/src/iosMain/kotlin/com/yourcompany/MainViewController.kt
package com.yourcompany

import androidx.compose.ui.window.ComposeUIViewController
import com.yourcompany.App

fun MainViewController() = ComposeUIViewController { 
    App() 
}

常见问题

问题1: Framework not found

bash 复制代码
解决方案:确保已执行 ./gradlew :shared:linkDebugFrameworkIosArm64
并且在Xcode中正确link了Framework

问题2: 编译时间过长

复制代码
首次编译可能需要5-10分钟
建议:使用Clean Build可提高后续编译速度

expect/actual机制详解

expect/actual是KMP处理平台差异的核心机制。

基本用法

1. 定义expect(commonMain)
kotlin 复制代码
// commonMain/kotlin/platform/Platform.kt
expect fun getPlatformName(): String
expect fun getVersion(): String
expect class FileManager(path: String) {
    fun exists(): Boolean
    fun readText(): String
}
2. 实现actual(各平台)

Android实现:

kotlin 复制代码
// androidMain/kotlin/platform/Platform.android.kt
actual fun getPlatformName(): String = "Android"

actual fun getVersion(): String {
    return android.os.Build.VERSION.SDK_INT.toString()
}

actual class FileManager(private val path: String) {
    private val file = java.io.File(path)
    
    actual fun exists(): Boolean {
        return file.exists()
    }
    
    actual fun readText(): String {
        return file.readText()
    }
}

iOS实现:

kotlin 复制代码
// iosMain/kotlin/platform/Platform.ios.kt
actual fun getPlatformName(): String = "iOS"

actual fun getVersion(): String {
    return UIDevice.currentDevice.systemVersion
}

actual class FileManager(private val path: String) {
    actual fun exists(): Boolean {
        return NSFileManager.defaultManager.fileExistsAtPath(path)
    }
    
    actual fun readText(): String {
        val string = NSString.stringWithContentsOfFile(path)
        return string ?: ""
    }
}

实际应用场景

场景1:文件存储路径
kotlin 复制代码
// commonMain
expect fun getCacheDir(): String

// androidMain
actual fun getCacheDir(): String {
    return context.cacheDir.absolutePath
}

// iosMain
actual fun getCacheDir(): String {
    val documentsPath = NSSearchPathForDirectoriesInDomains(
        NSDocumentDirectory, NSUserDomainMask, true
    )
    return documentsPath[0] as String
}
场景2:图片加载
kotlin 复制代码
// commonMain
@Composable
expect fun AsyncImage(
    url: String,
    contentDescription: String?,
    modifier: Modifier
)

// androidMain
actual fun AsyncImage(
    url: String,
    contentDescription: String?,
    modifier: Modifier
) {
    // 使用Coil加载图片
    AsyncImage(
        model = url,
        contentDescription = contentDescription,
        modifier = modifier
    )
}

// iosMain
actual fun AsyncImage(
    url: String,
    contentDescription: String?,
    modifier: Modifier
) {
    // 使用SDWebImage或其他iOS库
}
场景3:本地数据存储
kotlin 复制代码
// commonMain
expect class Preferences {
    fun putString(key: String, value: String)
    fun getString(key: String): String?
}

// androidMain
actual class Preferences(private val context: Context) {
    private val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
    
    actual fun putString(key: String, value: String) {
        prefs.edit().putString(key, value).apply()
    }
    
    actual fun getString(key: String): String? {
        return prefs.getString(key, null)
    }
}

// iosMain
actual class Preferences {
    private val userDefaults = NSUserDefaults.standardUserDefaults
    
    actual fun putString(key: String, value: String) {
        userDefaults.setString(value, forKey = key)
    }
    
    actual fun getString(key: String): String? {
        return userDefaults.stringForKey(key)
    }
}

expect/actual注意事项

  1. 类型兼容性 - expect和actual的类型必须完全匹配
  2. 可见性 - 确保expect函数/类的可见性与actual一致
  3. 默认参数 - expect中定义了默认参数,actual也必须包含
  4. 泛型参数 - 泛型的使用需要保持一致

资源管理

Compose Resources使用

KMP提供了跨平台资源系统:

资源文件结构:

arduino 复制代码
shared/src/commonMain/resources/MR/
├── strings/
│   └── values.default.strings
├── images/
│   └── logo.png
└── drawables/
    └── background.xml

strings.default.strings内容:

properties 复制代码
app_name = "AnimationDemo"
welcome_message = "Welcome to KMP"

代码使用:

kotlin 复制代码
import dev.icerock.moko.resources.compose.stringResource
import dev.icerock.moko.resources.compose.painterResource
import com.yourcompany.MR.strings
import com.yourcompany.MR.images

@Composable
fun WelcomeScreen() {
    Column {
        Text(stringResource(strings.welcome_message))
        Image(
            painter = painterResource(images.logo),
            contentDescription = stringResource(strings.app_name)
        )
    }
}

资源适配策略

iOS和Android对图片的格式要求不同:

  • Android:推荐使用WebP格式,支持透明通道(即可以自然融入背景)
  • iOS:推荐使用PNG格式
  • 建议:统一使用PNG格式以确保跨平台兼容

性能优化

Compose重组优化

使用remember缓存计算结果
kotlin 复制代码
// ❌ 不推荐:每次重组都重新计算
@Composable
fun ExpensiveComponent(data: Int) {
    val result = complexCalculation(data)  // 高计算成本
    Text("Result: $result")
}

// ✅ 推荐:使用remember缓存
@Composable
fun OptimizedComponent(data: Int) {
    val result = remember(data) {
        complexCalculation(data)
    }
    Text("Result: $result")
}
使用derivedStateOf避免不必要更新
kotlin 复制代码
@Composable
fun ListComponent(items: List<Item>) {
    // derivedStateOf只在items变化时重新计算
    val itemCount by remember {
        derivedStateOf { items.size }
    }
    Text("Items: $itemCount")
}

动画性能优化

限制并发动画数量,防止过载:

kotlin 复制代码
// 根据平台设置不同的最大动画数
const val MAX_ANIMATIONS = when (Platform.osFamily) {
    OsFamily.IOS -> 15
    OsFamily.ANDROID -> 10
    else -> 8
}

var activeAnimations by remember { mutableStateOf<List<Animation>>(emptyList()) }

fun addAnimation(animation: Animation) {
    activeAnimations = (activeAnimations + animation)
        .takeLast(MAX_ANIMATIONS)
}

@Composable
fun DisposableEffect(Unit) {
    onDispose {
        // 组件销毁时清理所有动画
        activeAnimations.clear()
    }
}

列表性能优化

使用LazyColumn替代Column:

kotlin 复制代码
// ❌ 不推荐:立即渲染所有项
@Composable
fun ListView(items: List<Item>) {
    Column {
        items.forEach { item ->
            ItemRow(item)
        }
    }
}

// ✅ 推荐:懒加载
@Composable
fun OptimizedListView(items: List<Item>) {
    LazyColumn {
        items(items) { item ->
            ItemRow(item)
        }
    }
}

开发调试技巧

平台特定日志

使用expect/actual实现跨平台日志:

kotlin 复制代码
// commonMain
expect fun logDebug(tag: String, message: String)

// androidMain
actual fun logDebug(tag: String, message: String) {
    Log.d(tag, message)
}

// iosMain
actual fun logDebug(tag: String, message: String) {
    NSLog("[%@] %@", tag, message)
}

性能监控

简单的FPS监控实现:

kotlin 复制代码
@Composable
fun PerformanceMonitor() {
    var frameCount by remember { mutableIntStateOf(0) }
    var fps by remember { mutableFloatStateOf(0f) }
    var lastTime by remember { mutableLongStateOf(System.currentTimeMillis()) }
    
    LaunchedEffect(Unit) {
        while (true) {
            delay(1000)
            val currentTime = System.currentTimeMillis()
            fps = frameCount * 1000f / (currentTime - lastTime)
            frameCount = 0
            lastTime = currentTime
        }
    }
    
    SideEffect {
        frameCount++
    }
    
    Text(
        "FPS: ${fps.toInt()}",
        color = Color.White,
        modifier = Modifier
            .background(Color.Black.copy(alpha = 0.7f))
            .padding(8.dp)
    )
}

常见问题与解决方案

编译问题

问题1: expect declaration has no corresponding actual

kotlin 复制代码
原因:定义了expect但缺少actual实现
解决:为每个目标平台添加actual实现

问题2: Framework not found: shared

ruby 复制代码
原因:iOS Framework未编译或路径错误
解决:执行 ./gradlew :shared:linkDebugFrameworkIosArm64

运行时问题

问题1: iOS键盘遮挡内容

kotlin 复制代码
// 解决方案:增加额外的padding
expect fun Modifier.keyboardSafePadding(): Modifier

// androidMain
actual fun Modifier.keyboardSafePadding(): Modifier = imePadding()

// iosMain
actual fun Modifier.keyboardSafePadding(): Modifier = padding(bottom = 80.dp)

问题2: 平台特定行为不一致

kotlin 复制代码
// 使用expect/actual统一接口
expect fun openUrl(url: String)

// androidMain
actual fun openUrl(url: String) {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
    context.startActivity(intent)
}

// iosMain
actual fun openUrl(url: String) {
    UIApplication.sharedApplication.openURL(
        NSURL.URLWithString(url)
    )
}

网络请求适配

Ktor配置

Ktor是JetBrains官方的KMP网络库,支持所有平台。

依赖配置:

kotlin 复制代码
// gradle/libs.versions.toml
[versions]
ktor = "2.3.5"

[libraries]
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }

// shared/build.gradle.kts
dependencies {
    commonMain.dependencies {
        implementation(libs.ktor.client.core)
        implementation(libs.ktor.client.content.negotiation)
        implementation(libs.ktor.serialization.kotlinx.json)
        implementation(libs.ktor.client.logging)
    }
    
    androidMain.dependencies {
        implementation(libs.ktor.client.android)
    }
    
    iosMain.dependencies {
        implementation(libs.ktor.client.darwin)
    }
}

HttpClient实现:

kotlin 复制代码
// commonMain
class ApiClient {
    private val client = HttpClient {
        install(ContentNegotiation) {
            json(
                contentType = ContentType.Application.Json,
                json = Json {
                    ignoreUnknownKeys = true
                    coerceInputValues = true
                }
            )
        }
        install(Logging) {
            logger = Logger.ANDROID
            level = LogLevel.INFO
        }
    }
    
    suspend fun getData(): Response<DataModel> {
        return client.get("https://api.example.com/data") {
            headers {
                append("Content-Type", "application/json")
            }
        }.body()
    }
    
    suspend fun postData(data: RequestModel): Response<String> {
        return client.post("https://api.example.com/data") {
            contentType(ContentType.Application.Json)
            setBody(data)
        }.body()
    }
}

API请求示例:

kotlin 复制代码
@Composable
fun DataScreen() {
    var data by remember { mutableStateOf<DataModel?>(null) }
    var loading by remember { mutableStateOf(false) }
    
    LaunchedEffect(Unit) {
        loading = true
        try {
            data = apiClient.getData()
        } catch (e: Exception) {
            logError(e)
        } finally {
            loading = false
        }
    }
    
    if (loading) {
        CircularProgressIndicator()
    } else {
        data?.let { DataView(it) }
    }
}

数据库适配(SQLDelight)

配置SQLDelight

依赖配置:

kotlin 复制代码
// shared/build.gradle.kts
plugins {
    id("app.cash.sqldelight") version "2.0.0"
}

sqldelight {
    databases {
        create("AppDatabase") {
            packageName.set("com.yourcompany.database")
            schemaOutputDirectory = file("src/commonMain/sqldelight/databases")
            verifyMigrations = true
        }
    }
}

dependencies {
    commonMain.dependencies {
        implementation("app.cash.sqldelight:runtime:2.0.0")
        implementation("app.cash.sqldelight:coroutines-extensions:2.0.0")
    }
    
    androidMain.dependencies {
        implementation("app.cash.sqldelight:android-driver:2.0.0")
    }
    
    iosMain.dependencies {
        implementation("app.cash.sqldelight:native-driver:2.0.0")
    }
}

定义Schema:

sql 复制代码
-- shared/src/commonMain/sqldelight/databases/User.sq
CREATE TABLE user (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL,
    created_at INTEGER NOT NULL
);

-- 查询
selectAll:
SELECT * FROM user;

insert:
INSERT INTO user (name, email, created_at)
VALUES (?, ?, ?);

delete:
DELETE FROM user WHERE id = ?;

使用数据库:

kotlin 复制代码
// 创建数据库实例
expect class DatabaseDriverFactory {
    fun createDriver(): SqlDriver
}

// androidMain
actual class DatabaseDriverFactory(private val context: Context) {
    actual fun createDriver(): SqlDriver {
        return AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
    }
}

// iosMain
actual class DatabaseDriverFactory {
    actual fun createDriver(): SqlDriver {
        return NativeSqliteDriver(AppDatabase.Schema, "app.db")
    }
}

// 使用
class UserRepository(private val database: AppDatabase) {
    suspend fun getAllUsers(): List<User> {
        return database.userQueries.selectAll().executeAsList()
    }
    
    suspend fun insertUser(user: User) {
        database.userQueries.insert(user.name, user.email, user.createdAt)
    }
}

依赖注入(Koin)

Koin配置

Koin支持KMP,可以进行依赖注入。

依赖配置:

kotlin 复制代码
// gradle/libs.versions.toml
koin = "3.5.0"

[libraries]
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }

// shared/build.gradle.kts
commonMain.dependencies {
    implementation(libs.koin.core)
    implementation(libs.koin.compose)
}

Koin模块定义:

kotlin 复制代码
// commonMain
val appModule = module {
    single { ApiClient() }
    single { UserRepository(get()) }
    factory { HomeViewModel(get(), get()) }
}

// 使用
fun initKoin() {
    startKoin {
        modules(appModule)
    }
}

ViewModel注入:

kotlin 复制代码
@Composable
fun HomeScreen() {
    val viewModel: HomeViewModel = getViewModel()
    val state by viewModel.state.collectAsState()
    
    when (state) {
        is State.Loading -> LoadingView()
        is State.Success -> DataView(state.data)
        is State.Error -> ErrorView(state.error)
    }
}

图片加载适配

Coil vs SDWebImage

不同平台使用不同的图片加载库。

定义统一的接口:

kotlin 复制代码
// commonMain
@Composable
expect fun AsyncImage(
    url: String,
    contentDescription: String?,
    modifier: Modifier,
    placeholder: Painter? = null
)

Android实现(Coil):

kotlin 复制代码
// androidMain
actual fun AsyncImage(
    url: String,
    contentDescription: String?,
    modifier: Modifier,
    placeholder: Painter?
) {
    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(url)
            .crossfade(true)
            .build(),
        contentDescription = contentDescription,
        placeholder = placeholder,
        modifier = modifier,
        contentScale = ContentScale.Crop
    )
}

iOS实现:

kotlin 复制代码
// iosMain
actual fun AsyncImage(
    url: String,
    contentDescription: String?,
    modifier: Modifier,
    placeholder: Painter?
) {
    // iOS端需要使用Compose for iOS的图片组件
    // 或者通过expect/actual桥接到SDWebImage
}

深度链接处理

kotlin 复制代码
// commonMain
expect class DeepLinkHandler {
    fun handleLink(url: String)
}

@Composable
fun rememberDeepLinkHandler(): DeepLinkHandler {
    return remember { DeepLinkHandler() }
}

// androidMain
actual class DeepLinkHandler {
    actual fun handleLink(url: String) {
        val uri = Uri.parse(url)
        val intent = Intent(Intent.ACTION_VIEW, uri)
        context.startActivity(intent)
    }
}

// iosMain
actual class DeepLinkHandler {
    actual fun handleLink(url: String) {
        UIApplication.sharedApplication.openURL(
            NSURL.URLWithString(url)!!
        )
    }
}

推送通知适配

使用expect/actual处理推送

kotlin 复制代码
// commonMain
expect class PushNotificationManager {
    fun requestPermission()
    fun sendLocalNotification(title: String, body: String)
}

@Composable
fun rememberPushManager(): PushNotificationManager {
    return remember { PushNotificationManager() }
}

// androidMain
actual class PushNotificationManager(private val context: Context) {
    actual fun requestPermission() {
        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE)
        // 请求通知权限
    }
    
    actual fun sendLocalNotification(title: String, body: String) {
        val notification = NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(title)
            .setContentText(body)
            .build()
        notificationManager.notify(NotificationIdGenerator.getId(), notification)
    }
}

// iosMain
actual class PushNotificationManager {
    actual fun requestPermission() {
        UNUserNotificationCenter.current().requestAuthorization(
            arrayOf(UNAuthorizationOptions.alert, UNAuthorizationOptions.badge)
        ) { _, _ -> }
    }
    
    actual fun sendLocalNotification(title: String, body: String) {
        val content = UNMutableNotificationContent().apply {
            this.title = title
            this.body = body
        }
        val request = UNNotificationRequest(
            identifier = "local",
            content = content,
            trigger = UNTimeIntervalNotificationTrigger(1, false)
        )
        UNUserNotificationCenter.current().add(request)
    }
}

设备信息获取

获取设备型号、系统版本等信息

kotlin 复制代码
// commonMain
data class DeviceInfo(
    val deviceName: String,
    val systemVersion: String,
    val manufacturer: String,
    val model: String
)

expect fun getDeviceInfo(): DeviceInfo

// androidMain
actual fun getDeviceInfo(): DeviceInfo {
    return DeviceInfo(
        deviceName = android.os.Build.DEVICE,
        systemVersion = android.os.Build.VERSION.RELEASE,
        manufacturer = android.os.Build.MANUFACTURER,
        model = android.os.Build.MODEL
    )
}

// iosMain
actual fun getDeviceInfo(): DeviceInfo {
    val device = UIDevice.currentDevice
    return DeviceInfo(
        deviceName = device.name,
        systemVersion = device.systemVersion,
        manufacturer = "Apple",
        model = device.model
    )
}

UI组件平台差异处理

TextField在不同平台的行为

iOS和Android的TextField在键盘、选择等方面有差异。

kotlin 复制代码
// commonMain
expect class PlatformTextFieldState {
    var value: String
    var focused: Boolean
}

@Composable
fun rememberPlatformTextFieldState(): PlatformTextFieldState

// androidMain
actual class PlatformTextFieldState {
    actual var value by mutableStateOf("")
    actual var focused by mutableStateOf(false)
}

actual fun rememberPlatformTextFieldState(): PlatformTextFieldState {
    return remember { PlatformTextFieldState() }
}

// iosMain
actual class PlatformTextFieldState {
    actual var value by mutableStateOf("")
    actual var focused by mutableStateOf(false)
}

深色模式适配

主题切换

kotlin 复制代码
// commonMain
enum class ThemeMode {
    Light, Dark, System
}

expect fun getCurrentThemeMode(): ThemeMode
expect fun setThemeMode(mode: ThemeMode)

// androidMain
actual fun getCurrentThemeMode(): ThemeMode {
    val nightModeFlags = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
    return when (nightModeFlags) {
        Configuration.UI_MODE_NIGHT_YES -> ThemeMode.Dark
        Configuration.UI_MODE_NIGHT_NO -> ThemeMode.Light
        else -> ThemeMode.System
    }
}

actual fun setThemeMode(mode: ThemeMode) {
    AppCompatDelegate.setDefaultNightMode(
        when (mode) {
            ThemeMode.Dark -> AppCompatDelegate.MODE_NIGHT_YES
            ThemeMode.Light -> AppCompatDelegate.MODE_NIGHT_NO
            ThemeMode.System -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
        }
    )
}

// iosMain
actual fun getCurrentThemeMode(): ThemeMode {
    val style = UITraitCollection.currentTraitCollection.userInterfaceStyle
    return when (style) {
        UIUserInterfaceStyle.light -> ThemeMode.Light
        UIUserInterfaceStyle.dark -> ThemeMode.Dark
        else -> ThemeMode.System
    }
}

actual fun setThemeMode(mode: ThemeMode) {
    // iOS通过SwiftUI处理
}

构建和打包配置

Android打包

bash 复制代码
# Debug构建
./gradlew :app:assembleDebug

# Release构建
./gradlew :app:assembleRelease

# 生成的APK位置
app/build/outputs/apk/release/app-release.apk

iOS打包

bash 复制代码
# 1. 编译Framework
./gradlew :shared:linkReleaseFrameworkIosArm64

# 2. 在Xcode中Archive
# Xcode → Product → Archive

# 3. 导出IPA
# Xcode → Distribute App

CI/CD配置

GitHub Actions示例:

yaml 复制代码
name: Build

on:
  push:
    branches: [ main ]

jobs:
  build-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          java-version: '11'
      - name: Build Android
        run: ./gradlew :app:assembleRelease
    
  build-ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build iOS Framework
        run: ./gradlew :shared:linkReleaseFrameworkIosArm64
      - name: Archive iOS
        run: xcodebuild archive ...

调试技巧补充

编译时错误提示

kotlin 复制代码
// 当expect/actual不匹配时的错误信息
// Error: Expect declaration has no corresponding actual
// 解决:检查是否在所有目标平台都实现了actual

运行时调试

使用断点时注意:

  • Android:直接在Android Studio断点
  • iOS:需要在Xcode中设置断点,或使用NSLog/print

日志统一管理

kotlin 复制代码
enum class LogLevel {
    DEBUG, INFO, WARNING, ERROR
}

expect fun log(level: LogLevel, tag: String, message: String, throwable: Throwable? = null)

// 使用统一的日志接口
fun debug(tag: String, message: String) = log(LogLevel.DEBUG, tag, message)
fun error(tag: String, message: String, e: Throwable) = log(LogLevel.ERROR, tag, message, e)

测试适配

单元测试

kotlin 复制代码
// commonTest
expect fun createTestDatabaseDriver(): SqlDriver

// androidTest
actual fun createTestDatabaseDriver(): SqlDriver {
    return InMemorySqlDriver(AppDatabase.Schema)
}

// iosTest
actual fun createTestDatabaseDriver(): SqlDriver {
    return InMemorySqlDriver(AppDatabase.Schema)
}

UI测试

kotlin 复制代码
@Composable
fun TestableApp() {
    val testMode = remember { BuildConfig.DEBUG }
    if (testMode) {
        TestHelper.setup()
    }
    App()
}

总结

关键技术点

  1. expect/actual机制 - 处理平台差异的核心
  2. Shared模块设计 - 业务逻辑与UI统一管理
  3. 资源系统 - 使用Compose Resources管理多平台资源
  4. 性能优化 - 注意重组控制和动画数量
  5. 网络库 - Ktor提供跨平台HTTP客户端
  6. 数据库 - SQLDelight提供类型安全的数据库访问
  7. 依赖注入 - Koin简化对象管理
  8. UI组件适配 - 处理平台差异的UI表现

最佳实践建议

  1. 优先选择新建KMP项目而非改造
  2. 熟悉expect/actual的使用场景
  3. 使用成熟的KMP生态库(Ktor、SQLDelight、Koin)
  4. 关注性能优化,特别是动画和列表
  5. 建立统一的调试和监控方案
  6. 逐步迁移,先验证核心功能
  7. 充分利用Compose Multiplatform的优势

学习资源


本文基于实际项目经验整理,涵盖了网络、数据库、依赖注入、推送、深度链接等多个方面的适配方案,如有问题欢迎讨论交流。

相关推荐
Carry3453 小时前
React 与 Vue 开发差异——CSS 样式
前端
從南走到北3 小时前
JAVA国际版任务悬赏发布接单系统源码支持IOS+Android+H5
android·java·ios·微信·微信小程序·小程序
前端九哥3 小时前
我删光了项目里的 try-catch,老板:6
前端·vue.js
2301_764441333 小时前
身份证校验工具
前端·python·1024程序员节
vistaup3 小时前
Android ContentProvier
android·数据库
我是场3 小时前
Android Camera 从应用到硬件之- 枚举Camera - 1
android
4Forsee3 小时前
【Android】View 事件分发机制与源码解析
android·java·前端
咕噜签名分发冰淇淋3 小时前
苹果ios安卓apk应用APP文件怎么修改手机APP显示的名称
android·ios·智能手机
应用市场3 小时前
从零开始打造Android桌面Launcher应用:原理剖析与完整实现
android