KMP(Kotlin Multiplatform)改造(Android/iOS)老项目
一、背景说明
新建KMP项目的情况下,无论是界面,还是业务逻辑都可以正常运行。但大多数情况下,我们是在原有项目基础上逐步改造,就需要把KMP项目作为依赖添加到原有项目中,并且保证KMP项目、原Android/iOS项目都能正常运行。
本文基于成功改造经验,分享改造过程,把遇到的坑以及解决过程分享给大家,供大家参考。
改造成功后,KMP项目无论跑Android/iOS/Desktop/Web都运行正常,原Android/iOS项目正常运行KMP中的Compose界面及业务逻辑,可做到KMP任意Compose页面嵌入到Android/iOS项目中。
二、Android改造
Android项目本来就支持Compose开发,并且编译打包脚本和KMP一模一样,但由于KMP模板项目把Android模块和公用Compose模块放在一起作为Application运行而不是单独Library库,这里就需要进行分离。如下原有KMP项目结构。
ComposeApp作为Android应用模块,也作为Compose公用模块使用,如果是androidApplication模块是无法作为库依赖到原有Android项目中的,因此要把ComposeApp变成androidLibrary模块,那改变之后原KMP就无法运行Android应用了,所以参考iosApp添加一个独立的入口模块就解决了。
2.1 KMP改造
2.1.1 打开compseApp文件夹目录,复制composeApp并命名为androidApp
2.1.2 在kmp项目的settings.gradle.kts中添加新的模块配置,添加 include(":AndroidApp") 如下所示
rootProject.name = "Capp_kmp"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")pluginManagement {}dependencyResolutionManagement {}plugins {id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}include(":AndroidApp")
include(":composeApp")
include(":server")
include(":shared")
2.1.3 同步gradle配置,删除新建androidApp目录下的commonMain、commonTest、desktopMain、iosMain、wasmJsMain目录。删除后androidApp如下
2.1.4 修改原composeApp模块,由应用变成依赖库。
-
打开composeApp下build.gradle.kts,找到plugins节点下
-
找到 alias(libs.plugins.androidApplication) 这一行改为 alias(libs.plugins.androidLibrary)
-
删除applicationId、versionCode、versionName,同步项目。
-
2.1.5 修改androidApp依赖composeApp
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTargetplugins {alias(libs.plugins.kotlinMultiplatform)alias(libs.plugins.androidApplication)alias(libs.plugins.composeMultiplatform)alias(libs.plugins.composeCompiler)alias(libs.plugins.composeHotReload)
}kotlin {androidTarget {@OptIn(ExperimentalKotlinGradlePluginApi::class)compilerOptions {jvmTarget.set(JvmTarget.JVM_11)}}sourceSets {androidMain.dependencies {implementation(compose.preview)implementation(libs.androidx.activity.compose)implementation(project(":composeApp"))}}
}android {namespace = "com.hbc.kmp.capp"compileSdk = libs.versions.android.compileSdk.get().toInt()defaultConfig {applicationId = "com.hbc.kmp.capp"minSdk = libs.versions.android.minSdk.get().toInt()targetSdk = libs.versions.android.targetSdk.get().toInt()versionCode = 1versionName = "1.0"}packaging {resources {excludes += "/META-INF/{AL2.0,LGPL2.1}"}}buildTypes {getByName("release") {isMinifyEnabled = false}}compileOptions {sourceCompatibility = JavaVersion.VERSION_11targetCompatibility = JavaVersion.VERSION_11}
}dependencies {debugImplementation(compose.uiTooling)
}
2.1.6 原有KMP项目composeApp模块的依赖修改
原kmp为下面projects.shared引用形式,但是在原Android项目中是不支持的,改为project路径命名形式不会影响KMP和Android。
KMP原有:implementation(projects.shared)
改为:implementation(project(":shared"))
2.1.7 编辑Android运行脚本
打开运行配置界面,修改Android运行模块,如下
运行模块指向新建的androidApp模块,这时候新的KMP模块即可正常编译运行在Android设备上了。
2.2 原Android项目依赖
2.2.1 原Android项目添加模块依赖
原有旧Android项目根目录settings.gradle中和rootProject.name同一级添加如下模块引入。
include(':composeApp')
project(':composeApp').projectDir = new File('../capp_KMP/composeApp')include(':shared')
project(':shared').projectDir = new File('../capp_KMP/shared')
注意File的路径一定要写正确,否则不能正确引入的。
2.2.2 显示KMP页面
在老项目中新建fragment,如下
package com.hugboga.custom.ui.homeimport android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import com.hbc.kmp.capp.Appclass HomeFragment : Fragment() {override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {return ComposeView(requireContext()).apply {setContent{App()}}}
}
把该fragment加入老项目任意位置即可显示。需要注意的是,原有老项目如果没有支持compose,需要添加对应配置。
implementation platform(libs.androidx.compose.bom)
implementation libs.androidx.activity.compose
implementation libs.androidx.ui.graphics
implementation libs.androidx.ui.tooling.preview
implementation libs.androidx.material3testImplementation libs.junit
androidTestImplementation platform(libs.androidx.compose.bom)
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
androidTestImplementation libs.androidx.ui.test.junit4
debugImplementation libs.androidx.ui.tooling
debugImplementation libs.androidx.ui.test.manifest
这里贴一下本项目的libs.versions.toml内容
[versions]
activity = "1.8.0"
activityKtx = "1.8.0"
agcp = "1.9.1.303"
agp = "8.10.1"
flexbox = "3.0.0"
gms = "4.4.0"
alibabacloudAndroidRumSdk = "0.3.12"
alibabacloudAndroidRumPlugin = "0.3.12"
arouterApi = "1.5.2"
arouterCompiler = "1.5.2"
androidsvg = "1.4"
annotation = "1.6.0"
asms = "1.8.7.2"
bannerviewpager = "3.5.12"
asplugin = "2.0.1.300"
broccoli = "1.0.0"
common = "9.8.0"
commonsCodec = "1.11"
conscryptAndroid = "2.5.0"
consecutivescroller = "4.6.4"
converterScalars = "2.9.0"
crashreport = "4.1.9.3"
easyconfig = "2.8.4"
danmakuflamemaster = "0.9.25"
fragmentKtx = "1.6.1"
googleServices = "4.4.0"
imageviewer = "1.1.0"
jverification = "3.2.5"
kotlin = "2.1.21"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
kotlinxCoroutinesCore = "1.7.3"
kotlinxCoroutinesAndroid = "1.7.3"
lifecycleViewmodelKtx = "2.6.1"
lifecycleRuntimeKtx = "2.6.1"
loggingInterceptor = "4.9.3"
lottie = "6.6.4"
marqueelibrary = "1.0.3"
material = "1.10.0"
recyclerview = "1.2.1"
fastjson = "2.0.57"
constraintlayout = "2.1.4"
mavericks = "3.0.5"
startupRuntime = "1.1.1"
nativecrashreport = "3.9.2"
converterGson = "2.11.0"
paletteKtx = "1.0.0"
refreshLayoutKernel = "3.0.0-alpha"
refreshHeaderClassics = "3.0.0-alpha"
scrolltextview = "V1.2.4"
shadowlayout = "3.3.2"
shadowdrawable = "0.1"
statusBarCompat = "0.7"
okhttp = "5.0.0-alpha.14"
okhttpIntegration = "5.0.0-rc01"
permissionx = "1.8.1"
retrofit = "2.11.0"
wechatSdkAndroid = "6.8.30"
uiAndroid = "1.8.3"
foundationLayoutAndroid = "1.8.3"
material3Android = "1.3.2"
composeHotReload = "1.0.0-alpha10"
composeMultiplatform = "1.8.1"
ktor = "3.1.3"
android-compileSdk = "36"
android-minSdk = "24"
android-targetSdk = "36"
androidx-activity = "1.10.1"
androidx-appcompat = "1.7.0"
androidx-constraintlayout = "2.2.1"
androidx-core = "1.16.0"
androidx-espresso = "3.6.1"
androidx-lifecycle = "2.9.0"
androidx-testExt = "1.2.1"
kotlinx-coroutines = "1.10.2"
coil = "3.2.0"
logback = "1.5.18"
compose-lifecycle = "2.9.0"
coroutines-core = "1.10.2"
fragment = "1.5.6"
ui = "1.8.3"
composeBom = "2024.09.00"[libraries]
agcp = { module = "com.huawei.agconnect:agcp", version.ref = "agcp" }
alibabacloud-android-rum-plugin = { module = "com.aliyun.rum:alibabacloud-android-rum-plugin", version.ref = "alibabacloudAndroidRumPlugin" }
alibabacloud-android-rum-sdk = { module = "com.aliyun.rum:alibabacloud-android-rum-sdk", version.ref = "alibabacloudAndroidRumSdk" }
androidsvg = { module = "com.caverock:androidsvg", version.ref = "androidsvg" }
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "startupRuntime" }
arouter-compiler = { module = "com.alibaba:arouter-compiler", version.ref = "arouterCompiler" }
arouter-api = { module = "com.alibaba:arouter-api", version.ref = "arouterApi" }
androidx-palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = "paletteKtx" }
asms = { module = "com.umeng.umsdk:asms", version.ref = "asms" }
bannerviewpager = { module = "com.github.zhpanvip:bannerviewpager", version.ref = "bannerviewpager" }
blueshield = { module = "com.mpaas.android:blueshield" }
broccoli = { module = "me.samlss:broccoli", version.ref = "broccoli" }
common = { module = "com.umeng.umsdk:common", version.ref = "common" }
commons-codec = { module = "commons-codec:commons-codec", version.ref = "commonsCodec" }
configservice = { module = "com.mpaas.android:configservice" }
conscrypt-android = { module = "org.conscrypt:conscrypt-android", version.ref = "conscryptAndroid" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
consecutivescroller = { module = "com.github.donkingliang:ConsecutiveScroller", version.ref = "consecutivescroller" }
crashreport = { module = "com.tencent.bugly:crashreport", version.ref = "crashreport" }
easyconfig = { module = "com.android.boost.easyconfig:easyconfig", version.ref = "easyconfig" }
converter-scalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "converterScalars" }
danmakuflamemaster = { module = "com.github.ctiao:DanmakuFlameMaster", version.ref = "danmakuflamemaster" }
fastjson = { module = "com.alibaba:fastjson", version.ref = "fastjson" }
flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" }
google-services = { module = "com.google.gms:google-services", version.ref = "googleServices" }
gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" }
asplugin = { module = "com.hihonor.mcs:asplugin", version.ref = "asplugin" }
imageviewer = { module = "com.github.jenly1314:imageviewer", version.ref = "imageviewer" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
jverification = { module = "cn.jiguang.sdk:jverification", version.ref = "jverification" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
logging = { module = "com.mpaas.android:logging" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" }
marqueelibrary = { module = "com.gongwen:marqueelibrary", version.ref = "marqueelibrary" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
recyclerview = {group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview"}
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
mavericks = { module = "com.airbnb.android:mavericks", version.ref = "mavericks" }
mriver-basic = { module = "com.mpaas.android:mriver-basic" }
nativecrashreport = { module = "com.tencent.bugly:nativecrashreport", version.ref = "nativecrashreport" }
nebula = { module = "com.mpaas.android:nebula" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-integration = { module = "com.github.bumptech.glide:okhttp-integration", version.ref = "okhttpIntegration" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
permissionx = { module = "com.guolindev.permissionx:permissionx", version.ref = "permissionx" }
refresh-header-classics = { module = "io.github.scwang90:refresh-header-classics", version.ref = "refreshHeaderClassics" }
refresh-layout-kernel = { module = "io.github.scwang90:refresh-layout-kernel", version.ref = "refreshLayoutKernel" }
scan_ai = { module = "com.mpaas.android:scan_ai" }
scrolltextview = { module = "com.github.Dkaishu:ScrollTextView", version.ref = "scrolltextview" }
shadowdrawable = { module = "com.github.Liberuman:ShadowDrawable", version.ref = "shadowdrawable" }
shadowlayout = { module = "com.github.lihangleo2:ShadowLayout", version.ref = "shadowlayout" }
status-bar-compat = { module = "com.githang:status-bar-compat", version.ref = "statusBarCompat" }
tinyapp = { module = "com.mpaas.android:tinyapp" }
tinyapp-media = { module = "com.mpaas.android:tinyapp-media" }
tinyapp-video = { module = "com.mpaas.android:tinyapp-video" }
video-player = { module = "com.mpaas.android:video-player" }
media = { module = "com.mpaas.android:media" }
uccore = { module = "com.mpaas.android:uccore" }
upgrade = { module = "com.mpaas.android:upgrade" }
wechat-sdk-android = { module = "com.tencent.mm.opensdk:wechat-sdk-android", version.ref = "wechatSdkAndroid" }
androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "uiAndroid" }
androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundationLayoutAndroid" }
androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { module = "junit:junit", version.ref = "junit" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "compose-lifecycle" }
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
ktor-serverCore = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" }
ktor-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
ktor-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
ktor-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
ktor-serverNetty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
ktor-serverTestHost = { module = "io.ktor:ktor-server-test-host-jvm", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-gson = { module = "io.ktor:ktor-gson", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-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" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
coil-network-ktor3 = { group = "io.coil-kt.coil3", name = "coil-network-ktor3", version.ref = "coil" }
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil"}
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines-core" }
androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "fragment" }
androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
google-services = { id = "com.google.gms.google-services", version.ref = "gms" }
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
如果觉得麻烦,直接在老项目新建Activity选择compose类型,项目compose依赖就自动添加到项目中了。
以上效果即是把KMP的页面嵌入在了现有Android项目中。
三、iOS改造
3.1 引入脚本
3.1.1 点击项目打开配置界面,添加自定义运行脚本
3.1.2 脚本内容
cd "../capp_KMP"
./gradlew :composeApp:embedAndSignAppleFrameworkForXcode
以上kmp路径一定要写正确,是kmp项目根目录路径,gradle脚本会编译子模块,自动把编译好的.framework文件添加到本项目。
3.1.3 脚本位置
有时候打开项目,kmp的依赖无法加载,这里调整了一下新加脚本Run Build KMP Frameworks的顺序,移动到了最上面就正常了。
3.2 添加Info.plist配置
在老项目Info.plist中添加如下配置
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
KMP项目需要解除屏幕刷新频率限制,从iOS 15起,Apple默认第三方应用频率在60Hz,加入上述配置即可解除。
3.3 页面引用
3.3.1 老项目中新建ViewController
//
// KmpVc.swift
// cclx-mpaas-capp
//
// Created by 赵泓博 on 2025/6/30.
// Copyright © 2025 HBC. All rights reserved.
//import UIKit
import SwiftUI
import ComposeAppclass KmpVc: UIViewController {override func viewDidLoad() {var mainView = ComposeView()let host = UIHostingController(rootView: mainView)host.view.translatesAutoresizingMaskIntoConstraints = falseself.view.addSubview(host.view)self.addChild(host)NSLayoutConstraint.activate([host.view.topAnchor.constraint(equalTo: view.topAnchor),host.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),host.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)])}
}struct ComposeView: UIViewControllerRepresentable {func makeUIViewController(context: Context) -> UIViewController {MainViewControllerKt.MainViewController()}func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
这里ComposeView把KMP项目中composeApp模块下的iosMain中的ComposeUIViewController包装成iOS的UIViewController,此时KmpVc就显示了Compose中的页面。
3.3.2 任意compose页面引用
需要在composeApp项目,iosMain的MainViewController中进行compose包装,按照3.3.1中的方式进行引入iOS中即可。
四、总结
4.1 混合开发
以上KMP和原生Android/iOS的环境配置好以后,就可以尽情的在KMP项目中进行业务开发了,做到了开发一次,运行在Android/iOS平台,完全节省了一半人力和资源。
4.2 未来思考
当然,很多比较特殊的场景,可能在未来会遇到很多坑,但相信一定可以解决。如页面跳转等,未来会持续分享给大家。