KMP跨平台开发实战:从Android单端到多平台适配
本文基于实际项目经验,详细介绍Kotlin Multiplatform (KMP) 的接入方式、平台适配方案以及开发中遇到的技术问题与解决方案。
引言
在移动应用开发中,往往需要同时维护Android和iOS两个平台的应用。传统方案需要分别用Kotlin和Swift开发,导致业务逻辑重复、维护成本高。KMP (Kotlin Multiplatform) 提供了共享业务逻辑和UI代码的解决方案。
经过实际项目验证,采用KMP后可以实现约70-80%的代码复用,显著降低开发和维护成本。
项目创建方式
方式1:使用Android Studio模板创建(推荐)
创建步骤:
- 打开
File → New → New Project - 选择
Kotlin Multiplatform Mobile - 填写项目信息:
- Project Name: AnimationDemo
- Package: com.yourcompany.anim
- Minimum SDK: API 24
- iOS Deployment Target: 13.0
- 选择模板:
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注意事项
- 类型兼容性 - expect和actual的类型必须完全匹配
- 可见性 - 确保expect函数/类的可见性与actual一致
- 默认参数 - expect中定义了默认参数,actual也必须包含
- 泛型参数 - 泛型的使用需要保持一致
资源管理
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
}
深度链接处理
处理Scheme和Universal Link
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()
}
总结
关键技术点
- expect/actual机制 - 处理平台差异的核心
- Shared模块设计 - 业务逻辑与UI统一管理
- 资源系统 - 使用Compose Resources管理多平台资源
- 性能优化 - 注意重组控制和动画数量
- 网络库 - Ktor提供跨平台HTTP客户端
- 数据库 - SQLDelight提供类型安全的数据库访问
- 依赖注入 - Koin简化对象管理
- UI组件适配 - 处理平台差异的UI表现
最佳实践建议
- 优先选择新建KMP项目而非改造
- 熟悉expect/actual的使用场景
- 使用成熟的KMP生态库(Ktor、SQLDelight、Koin)
- 关注性能优化,特别是动画和列表
- 建立统一的调试和监控方案
- 逐步迁移,先验证核心功能
- 充分利用Compose Multiplatform的优势
学习资源
本文基于实际项目经验整理,涵盖了网络、数据库、依赖注入、推送、深度链接等多个方面的适配方案,如有问题欢迎讨论交流。