十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

KMP-CMP跨端实践:一套Kotlin代码搞定Android与iOS

KMP-CMP跨端实践:一套Kotlin代码搞定Android与iOS 做移动端跨端方案选型绝大多数团队的第一反应都是 Flutter。我不否认 Flutter 的优秀但从 2023 年开始我所在团队走了一条不太一样的路KMP-CMP也就是 Kotlin Multiplatform 加 Compose Multiplatform用一套 Kotlin 代码同时覆盖 Android 和 iOS并且在 App 内部用 SPA 的单容器架构来组织全部页面。这套路线目前已经稳定支撑了三个线上版本双端 UI 共享率接近九成我想把这段工程实践完整拆出来给正在做技术选型或者对 KMP-CMP 跃跃欲试的同行一个真实参考。先说结果再说适合谁。KMP-CMP 的核心价值不是“完全替代 Flutter”而是在 Kotlin 生态里提供一条 UI 层也能共享的工程路线业务逻辑、数据层、页面 UI 全部写一份双端运行。它适合已经有 Android 技术积累、又被 iOS 交付成本压得喘不过气来的团队也适合那些对原生能力依赖较强、App 里有大量蓝牙、地图、硬件交互的业务场景。如果你还没接触过 Kotlin 或 Compose这篇文章里的代码示例会帮你避开我踩过的那些坑。1. 路线选型为什么是 KMP-CMP 而不是 Flutter1.1 KMP 和 CMP 到底是什么关系很多人把 KMP 和 CMP 混在一起说其实它们是两个层面。KMP 是 Kotlin Multiplatform 的缩写解决的是“业务逻辑跨端共享”的问题一套用 Kotlin 写的网络层、数据仓库、领域模型可以编译成 Android 的字节码和 iOS 的 Framework在各自平台被原生代码调用。CMP 是 Compose Multiplatform 的缩写是 JetBrains 在 Jetpack Compose 基础上做的跨端 UI 实现把 Android 的声明式 UI 范式搬到了 iOS 上。KMP 负责“脑子”CMP 负责“脸”两者合起来才叫“全页面跨端 UI”。这个组合的关键在于它和 Flutter 共享同一个目标就是“一份代码双端运行”但底层逻辑完全是另一套。Flutter 用的是 Dart 语言加自绘渲染引擎整个控件树都是自己画的等于在系统之上又造了一个完整操作系统KMP-CMP 则站在 Kotlin 和平台原生控件肩膀上通过 Compose 编译器把 UI 描述翻译成平台可执行的绘制指令所以和原生代码的互操作天然更顺畅。我要再强调一点CMP 并不是 KMP 的唯一 UI 方案。KMP 完全可以只共享逻辑层UI 层还是用 SwiftUI 加 Jetpack Compose 分别写。这种方案很多团队在用但它没有解决“UI 双份维护”的痛点。我们选择 CMP就是为了把最后一个双份成本也干掉。1.2 与 Flutter 的核心差异生态还是护城河选 Flutter 还是 KMP-CMP本质上是在选生态。Flutter 的生态里Dart 是唯一语言包管理器是 pubUI 组件全部来自 Flutter 体系KMP-CMP 的生态里Kotlin 是 JVM 语言家族成员背后有 JetBrains 和 Google 双重背书UI 组件是 Jetpack Compose 的跨端延伸。这意味着什么意味着你团队里的 Android 开发者转型 KMP-CMP 几乎零成本Android 上的 Compose 经验直接迁移到 iOS而 Flutter 要求所有成员重新学一门语言加一个新 UI 框架这条学习曲线不是几个月能抹平的。我做一个团队常看的对比表格对比维度FlutterKMP-CMP开发语言Dart需要团队重新学习KotlinAndroid 团队直接上手UI 渲染自绘引擎Skia/Impeller脱离平台控件平台原生渲染贴近系统体验原生互操作通过 MethodChannel/Pigeon 桥接直接调用原生 APIexpect/actual 模式热重载体验优秀状态保留稳定尚可Compose 预览在 iOS 端不如 Android 顺手动态化/热更新受限需要额外方案与原生一致走发布流程团队技术积累迁移成本高几乎从头开始低Android 知识直接复用跨端 UI 共享度100%可接近 100%部分平台差异需 expect/actual我没有说 Flutter 不好它生态成熟、社区活跃、招聘市场上人也多。但对我们这种 Android 基因很强的团队来说KMP-CMP 的学习成本几乎是 Flutter 的三分之一。花同样的时间我们已经上线了业务隔壁组还在调 Dart 的异步模型。1.3 适合走这条路的团队画像和业务特征如果你正在做技术选型先别急着抄我们的方案。KMP-CMP 目前有几个前提条件缺一条我都不建议硬上。第一团队里至少有 2 个能写 Kotlin 的人。CMP 虽然是跨端但最终调用的还是原生 APIAndroid 和 iOS 的平台差异依然存在你得有人能看懂两端日志。第二业务场景需要对原生能力有较强依赖比如低功耗蓝牙、硬件 SDK、地图服务。这类场景 Flutter 也能做但每接一个原生 SDK 都要写一次 MethodChannel 桥接代码量大不说排查问题还费劲。第三领导层愿意接受渐进式迁移而不是指望推翻重来。我们当时就是从 Android App 里先抽出数据层接 KMP再逐步把页面迁到 CMP整个过程没有大规模重写。至于纯 UI 展示型产品、快速验证原型的场景我反而建议 Flutter 更省事因为它的生态里现成的轮子实在太多了从 UI 库到状态管理一应俱全开箱即用。2. 移动端 SPA 架构设计单容器承载全部页面2.1 移动端为什么要做 SPA 化SPA 是 Single Page Application 的缩写原本是 Web 前端的概念指整个应用只有一个 HTML 页面页面切换靠前端路由完成。我把它搬到了移动端App 内部只有一个原生容器页所有业务页面都是容器内的 Compose 组件页面跳转完全由路由表控制不再新建原生 Activity 或者 ViewController。为什么这么干核心原因是双端生命周期统一。传统多 Activity 架构在 Android 上很自然back 栈、任务栈都是系统管好的到了 iOS 就完全不一样View Controller 的压栈出栈语义和 Android 对不上跨端共享代码时导航逻辑每端写一遍互相之间还没法复用。用 SPA 单容器架构之后路由表变成一份纯 Kotlin 数据页面切换、参数传递、返回手势全部统一处理双端行为一致率大幅提升。另一个好处是内存控制。移动端 SPA 的单容器本质是用一个原生页承载 Compose 的组件树页面退出时组件从组合中移除对应的资源直接释放。相比传统多页面栈页面销毁更彻底不容易出现页面栈越积越深导致的内存膨胀问题。这和热词里大家关心的“移动端性能优化”正好是同一类话题。2.2 路由方案选型Voyager、Decompose 还是自研移动端 SPA 最核心的是路由。我们调研过三个方案。第一个是 JetBrains 推荐的 Voyager它提供 Screen、Navigator、TabNavigation 等抽象API 设计非常贴近 Compose 的声明式风格写起来最舒服。但它的缺陷是页面参数传递是类型安全的序列化方式跨模块通信需要提前定义好路由协议我们业务里有大量动态下发页面配置的场景处理起来有点绕。第二个是 Badoo 开源的 Decompose它把状态管理和导航拆成独立组件支持复杂的嵌套导航、跨平台生命周期、back 手势处理。功能很强但学习成本不低抽象层级多新同学上手比较慢。第三个就是我们最终的选择在 Voyager 基础上包了一层自研路由表。核心思路是把页面路径注册成字符串常量每个页面组件对应一个路由节点参数用 Kotlin 的 data class 封装。这样做的好处是路由寻址可以和后端下发的页面链接打通运营配置的 H5 跳转原生页面也能直接命中路由节点。自研路由的简化模型长这样data class RouteNode( val path: String, val screen: Composable (RouteArgs) - Unit, val needLogin: Boolean false ) class SPAApp : Application() { val routeTable listOf( RouteNode(/home, { HomeScreen(it) }), RouteNode(/profile, { ProfileScreen(it) }, needLogin true), RouteNode(/order/detail, { OrderDetailScreen(it) }, needLogin true) ) }页面跳转时只需要根据 path 查表得到对应 Composable再传入解析好的 RouteArgs 即可。整个方案轻盈、可控后续加动态化配置也容易。2.3 shared 模块的分层与平台差异隔离SPA 架构确定后紧接着就是代码分层。我们用标准的三层结构domain 层放纯 Kotlin 的领域模型和业务规则data 层放网络、数据库、偏好存储等数据源presentation 层放 ViewModel 加 Compose UI。分层里最要命的是平台差异隔离这个 Google 和 JetBrains 已经给了标准方案就是 expect/actual 机制。expect 声明一个公共接口actual 在 Android 和 iOS 各自实现。举个例子App 里需要读取设备型号域名层的接口只暴露一个函数// commonMain expect fun getDeviceModel(): String// androidMain actual fun getDeviceModel(): String { return Build.MODEL ?: unknown }// iosMain actual fun getDeviceModel(): String { return UIDevice.currentDevice.model }业务代码完全不用关心它在两端是怎么实现的。我强烈建议团队里新写的公共 API 一开始就按 expect/actual 设计不要等项目跑起来再补后期拆平台差异的代价远大于前期设计成本。3. 工程落地与核心实现3.1 初始化 KMP-CMP 工程的正确姿势工程初始化这一步十个人有九个会踩版本坑我直接说结论。Kotlin Multiplatform 的插件版本、Compose Multiplatform 插件版本、AGP 版本、Gradle 版本是强绑定关系不是越新越好。我有一个自己的版本匹配原则Kotlin 用稳定版压一年CMP 用跟随 Kotlin 主版本发布的稳定版AGP 用 Android Studio 推荐的最低版本。以我当前项目为例核心版本号是// 顶层 build.gradle.kts plugins { kotlin(multiplatform) version 2.0.21 id(org.jetbrains.compose) version 1.7.3 id(com.android.application) version 8.7.3 }创建工程时别手动搭 Gradle 结构直接用 JetBrains 官方的 KMP Wizard 生成它会根据你选的配置自动匹配一套能跑的版本组合。生成之后再逐步升级出问题也容易定位。模块结构建议这样组织shared/ src/ commonMain/kotlin/ // 公共代码路由、页面、业务逻辑 androidMain/kotlin/ // Android 平台实现 iosMain/kotlin/ // iOS 平台实现 build.gradle.ktsCommon 模块的 build.gradle.kts 里记得把所有平台 target 配全kotlin { androidTarget { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } listOf( 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(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(libs.ktor.client.core) implementation(libs.kotlinx.coroutines.core) } androidMain.dependencies { implementation(compose.preview) implementation(libs.androidx.activity.compose) } iosMain.dependencies { implementation(libs.ktor.client.darwin) } } }这里提一个很多人会忽略的点iosArm64()和iosSimulatorArm64()都要配上前者是真机后者是 Apple Silicon 模拟器。如果漏了模拟器 target你在 Mac 上跑 iOS 模拟器时就会发现 framework 根本编不出来。3.2 全页面跨端 UI 的 Compose 组件实践CMP 的 UI 代码写起来和 Jetpack Compose 几乎一样但跨端之后会出现一些细微差异。我从实践中总结出三条最实用的经验。第一条Material3 组件在 iOS 上不要无脑用。比如TopAppBar在 Android 上默认自带返回箭头和状态栏占位到了 iOS 上状态栏高度策略不同标题栏会偏高或者和刘海重叠。我们的做法是自定义一个CommonTopBar用 expect/actual 处理 top insetAndroid 返回 24.dpiOS 返回安全区高度加状态栏高度。这样双端观感才能一致。第二条列表性能是 UI 跨端的生命线。CMP 的LazyColumn性能已经不错但要注意 item 必须设置key否则列表滑动时重组成本会指数级上升。另外图片加载用 Coil 3 的跨端版本它在 KMP-CMP 下可以共用一套图片加载代码还能天然支持网络图片缓存和占位图。第三条字体渲染差异。iOS 的字体渲染比 Android 细同样的 16.sp 在 iOS 上看起来比 Android 小。这不是 Bug是平台的系统字体差异。我们会在公共主题层统一调节iOS 上全局字重加粗一点点观感就拉回来了。一个典型的页面组件大概长这样Composable fun ProfileScreen(vm: ProfileViewModel viewModel()) { val state by vm.uiState.collectAsState() CommonTopBar(title 个人中心) LazyColumn( modifier Modifier.fillMaxSize(), contentPadding PaddingValues(16.dp), verticalArrangement Arrangement.spacedBy(12.dp) ) { items(state.orderList, key { it.id }) { order - OrderCard(order order) } } }3.3 数据层与网络请求封装移动端 SPA 的数据层是整个架构的地基。我们选了 Ktor Client 作为跨端网络库它是 JetBrains 出品和 KMP-CMP 同源配合度最好。Android 端引擎用 OkHttpiOS 端用 Darwin 引擎两端行为差异极小。这里要说一个热词里很多人关心的点“spa项目开发之jwt验证码实现”。移动端 SPA 和 Web SPA 一样要做鉴权我们用 JWT 加验证码的组合。验证码图片在登录页用 Compose 自绘加随机干扰线实现Token 采用双 Token 策略Access Token 短时效Refresh Token 长时效网络层拦截 401 后自动刷新重放请求。Ktor 的拦截器机制写起来非常顺手class AuthHandler(private val tokenStore: TokenStore) { fun handleRequest(): HttpRequestBuilder.() - Unit { tokenStore.getAccessToken()?.let { token - header(HttpHeaders.Authorization, Bearer $token) } } fun handleResponse(): HttpResponseValidator object : HttpResponseValidator { override fun validateResponse(response: HttpResponse) { if (response.status.value 401) { // 统一刷新 Token刷新失败则踢回登录页 } } } }TokenStore 用 expect/actual 实现Android 端用 DataStore 存储iOS 端用 NSUserDefaults业务层直接调用统一接口不用关心底层存储差异。3.4 与原生能力互操作蓝牙、地图、推送这类场景怎么处理KMP-CMP 最大的杀手锏就是原生互操作。拿低功耗蓝牙举例子热词里有人在问“flutter 低功耗蓝牙 iOS 有问题嘛”。Flutter 在这块确实坑不少因为它的 Dart 层到原生蓝牙框架之间隔了一层桥每次调用都要走二进制消息通道出问题后要同时排查 Dart、桥接层和原生三层代码。KMP-CMP 是直接调用原生蓝牙 APIAndroid 端在 androidMain 里写 BluetoothGatt 操作iOS 端在 iosMain 里写 CoreBluetooth 操作中间没有任何桥接层排查问题时一条链路直接通到底。我们的蓝牙模块设计是这样的commonMain 定义BLEManager接口提供扫描、连接、读写特征值的抽象androidMain 用系统蓝牙 API 实现iosMain 用 CoreBluetooth 实现。业务页面只关心 commonMain 的接口状态比如蓝牙连接状态用 StateFlow 暴露给 UI 层// commonMain interface BLEManager { val connectionState: StateFlowConnectionState fun scan() fun connect(deviceId: String) fun write(data: ByteArray) }同类问题还有地图。我们接入过天地图也用过高德地图的 Flutter 插件但地图这种涉及大量手势交互和原生绘制的场景Flutter 插件的维护成本一直在涨。KMP-CMP 下地图可以直接封装进 shared 模块Android 端用高德原生的 Fragment 嵌入 Compose AndroidViewiOS 端用 MKMapView 嵌入 Compose UIKitView两端各写一份包装实现但业务调用方式和坐标系换算逻辑全部共用一套 Kotlin 代码。有一些非常常识的坑iOS 蓝牙权限必须在 Info.plist 里声明NSBluetoothAlwaysUsageDescription定位权限要声明NSLocationWhenInUseUsageDescription这类权限声明不能用 expect/actual 统一它属于平台配置忘了加的话运行时会直接崩溃。4. 性能优化与稳定性治理4.1 启动链路与首屏优化移动端 SPA 的性能优化重点和原生 App 不一样。由于只有一个容器页冷启动时整个 Compose 组件树都要在容器里构建首屏渲染路径比其他架构更长所以“启动优化”其实就是“首屏组件树瘦身”。我们的做法有四个。第一路由表懒加载不把全部页面组件一次性注册到导航器里只有一个 Map 存路径到构造函数的映射真正导航到该页面时才创建。第二首屏页面用remember { }加 ViewModel 懒加载网络请求在页面组合完成后再触发不阻塞 Compose 初始化。第三SPA 容器页的原生背景色和启动页保持一致让用户在跳进容器页的瞬间感受不到白屏闪烁。第四所有页面的图片资源默认用 Compose 的尺寸限定符做多倍图适配减少首屏解码压力。iOS 上还有一个额外的优化点CMP 生成的 framework 如果是动态库启动时 Linker 会花时间加载符号表。我们直接改用静态库配置启动时间能缩短 10% 到 15%代价是包体积会小幅上涨但在业务可控的范围内。4.2 列表性能与图片内存治理热词里反复出现的“flutter内存优化”和“移动端性能优化”在 KMP-CMP 下同样适用但排查路径完全不一样。CMP 的内存模型和 Android 原生一致可以用 Android Studio 的 Profiler 直接看堆内存不像 Flutter 还要先转 Dart 层再做实例分析省了不少功夫。列表性能上我们遇到过最典型的问题是LazyColumn在 iOS 上快速滑动时掉帧。定位后发现不是 CMP 渲染问题而是图片库默认的磁盘缓存策略在 iOS 上太保守滑动时频繁做解码。换用 Coil 3 并手动调大内存缓存比例后问题直接消失。图片内存还有一种病态场景网络图片的宽高信息和实际展示尺寸不一致导致图片加载后先按原尺寸解码再压缩内存瞬间飙升。我们在网络层统一加了Resize转换器加载前就拿到目标尺寸解码时就按目标尺寸解码。以下是一个简化写法AsyncImage( model ImageRequest.Builder(LocalPlatformContext.current) .data(imageUrl) .size(400, 400) .crossfade(true) .build(), contentDescription null )4.3 协程多线程KMP-CMP 的并行处理方案Flutter 的多线程靠 Isolate写起来要专门设计通信协议KMP-CMP 直接用 Kotlin 协程API 在各个平台是一致的。这里有个很容易出错的地方协程的Dispatchers.IO在不同平台上的实际实现不同。Android 上是基于 OkHttp 的线程池封装iOS 上是 coroutines 在 Darwin 下用 Grand Central Dispatch 实现。写业务代码时永远不要在 commonMain 里硬编码线程模型而是定义一个公共的调度器// commonMain expect val appDispatcher: CoroutineDispatcherAndroid 实现用Dispatchers.IOiOS 实现用Dispatchers.Default。这样保证 commonMain 里的withContext(appDispatcher)在两端都不会阻塞 UI 线程。我们线上遇到过一次诡异的崩溃在 iOS 上往一个MutableStateFlow里发数据时后台线程没经过主线程调度直接触发 UI 重组导致并发修改崩溃。排查后确认是某个工具类里用了GlobalScope.launch没有指定主线程。后来全局扫了一遍所有协程入口统一用MainScope创建杜绝了这类并发问题。5. 常见问题与排查技巧实录5.1 构建期Gradle、AGP、Kotlin 版本强绑定构建期最大的坑是版本矩阵。KMP 和 CMP 插件对 Kotlin 编译器版本非常敏感经常出现“前一天还能编译升级一个插件后连不上”的情况。症状通常是e: Supertypes of the following classes cannot be resolved这类报错第一次遇到会误以为是业务代码写错了。我的排查路径是固定的先看 Gradle 警告里的 Plugin 版本检查输出再比对 Kotlin 版本和 CMP 版本的生命周期对应关系。JetBrains 在 GitHub Releases 页面有版本兼容矩阵我每次升级前先核对一遍再动手。另外遇到过和热词里那条“you are applying flutter‘s main gradle plugin imperatively using the apply”类似的报错。KMP 工程里如果同时混了 Android App 模块和 KMP 模块很容易出现插件重复应用或者应用顺序混乱。解决办法就是在根项目的 settings.gradle.kts 里用pluginManagement统一声明插件仓库和版本所有模块只通过alias引用插件坚决不用apply plugin命令式写法。5.2 iOS 构建CocoaPods、Xcode 和 Framework 配置KMP 生成的 iOS Framework 在集成到 Xcode 工程时有两种方式CocoaPods 和 Swift Package Manager。我们用 CocoaPods 更稳因为它对动态库和静态库的支持更成熟和 CMP 的集成文档也是首先覆盖这条路。一个常见的坑Xcode 真机编译要用iosArm64的 framework模拟器编译要用iosSimulatorArm64如果只生成了一种会在构建时报building for iOS Simulator, but linking in object file built for iOS。写脚本在编译前动态选择 framework 时要注意区分目标 SDKif [ $PLATFORM_NAME iphonesimulator ]; then SHARED_FRAMEWORK_DIR$(PWD)/shared/build/xcode-frameworks/$(CONFIGURATION)/iosSimulatorArm64 else SHARED_FRAMEWORK_DIR$(PWD)/shared/build/xcode-frameworks/$(CONFIGURATION)/iosArm64 fi这问题在热词里被反复问到是有原因的。如果你在本地跑脚本没问题一上 CI 就报错八成就是忘了区分模拟器和真机的 SDK 路径。5.3 鸿蒙适配KMP-CMP 的下一站热词里已经出现“kmp 鸿蒙适配”这是真实存在的趋势。HarmonyOS NEXT 启动后很多 Android 团队在评估 KMP 能不能延伸到鸿蒙。目前社区有 OpenHarmony 的 Kotlin Multiplatform 适配项目Kotlin/Native 目标里也加入了ohosArm64相关支持。但我要提醒目前 CMP 对鸿蒙的 UI 自适应还处于早期阶段Compose 编译器在鸿蒙上的渲染后端还没有完全稳定生产环境大规模使用仍需观望。我的建议是如果鸿蒙是你未来半年的核心交付目标可以先在数据层用 KMP 做跨端等 CMP 在鸿蒙上的适配成熟后再迁 UI。渐进式的好处在这种时刻就体现出来了不至于被单一路线绑死。5.4 性能定位工具一套组合拳打天下跨端开发的排查工具链是最容易让人头秃的。Flutter 有自己的一套 DevTools但跨端桥接问题定位起来依然费劲。KMP-CMP 开发时的性能定位可以用三套工具组合。Android 端没什么好说的Android Studio 自带的 Profiler 直接看 CPU、内存、网络和原生开发体验完全一致。iOS 端用 Instruments 的 Time Profiler 和 Allocations 模板能直接看到 Kotlin 协程调度到 GCD 队列上的执行情况。跨端公共代码的内存问题用 Kotlin/Native 的 Memory Profiler它能看到 commonMain 里的对象分配是排查泄漏的利器。6. 团队落地建议与演进路径6.1 渐进式迁移的三个阶段如果有人问我要不要全员 All-in KMP-CMP我给出的答案永远是分三步走。第一阶段只共享数据层。把现有 Android App 的网络请求、数据仓库、登录状态迁到 shared 模块iOS 端暂时继续用原生代码但数据模型从 shared 里引用。这个阶段收益低但风险极低团队能先跑通 KMP 的日常开发流程。第二阶段共享业务逻辑加部分 UI。选两三个无复杂原生依赖的页面迁到 CMP比如个人中心、设置页、订单列表。这个阶段的主要目标是验证 CMP 的双端 UI 一致性和团队对声明式 UI 的熟练度。第三阶段全页面跨端 UI 加 SPA 化。等团队对 CMP 有足够信心后再逐步把原生页面替换成 Compose 页面最终收敛到单容器 SPA 架构。这个阶段要同步引入路由规范、组件库规范和原生能力封装规范不然并行开发时容易写出“一台代码两套风格”的怪胎。6.2 什么情况下建议继续用 Flutter我在前面说了很多 KMP-CMP 的好处但必须负责任地把“不适用场景”也讲清楚。如果你的 App 以内容展示为主页面里没有多少原生硬件交互团队没有 Kotlin 背景招人市场上 Flutter 开发者更容易招到那 KMP-CMP 的性价比就远远不如 Flutter。另一个典型场景是强动态化需求。Flutter 的产物是自绘渲染理论上可以在云端加载后本地运行但要真正把这个能力做出来复杂度极高两边其实都不适合最终都要靠原生层面的热更新体系。这种情况下选哪边都一样关键看谁的学习成本低。6.3 团队人员能力模型如何调整KMP-CMP 对团队成员的能力要求比 Flutter 更“叠 buff”。Android 开发要懂 iOS 的生命周期差异误以为 Kotlin 一把梭就不用管平台细节实际 expect/actual 写了多少就说明你对平台差异理解有多深。iOS 开发要能接受用 Kotlin 写 UI虽然 CMP 在 iOS 上能用的系统控件和手势 API 在逐步补齐但和 SwiftUI 的原生体验还是有差距。我们团队的做法是重新定义岗位不再分 Android 开发者和 iOS 开发者统一叫“跨端业务开发”但每个小组必须保留一个“平台工程师”负责维护 androidMain 和 iosMain 里的平台实现。这样既不牺牲专业深度又能让业务代码的共享率持续提高。我个人非常看好这条路线接下来的走势。KMP 已经是 Google 在 Android 官方文档里推荐的跨平台逻辑共享方案CMP 的 iOS 稳定版发布后JetBrains 的迭代速度快得惊人。如果你所在团队正在纠结跨端选型不妨先拿出一个最小功能模块用 KMP-CMP 做个 2 到 4 周的可行性验证再用数据说话。我当时就是靠一个登录页加个人中心说服了整个团队从 Flutter 转向我们今天在用的这套架构。
返回列表