2025年还在观望KMP的人,基本可以放弃犹豫了。Kotlin Multiplatform从当年的“玩具框架”走到今天,已经能在实际商业项目里扛起核心业务逻辑的多端复用了。我过去两年在几个App项目里陆续把账号体系、数据层、网络层迁到了KMP,最近又在用Compose Multiplatform尝试共享UI层,踩了不少坑,也沉淀了一套可复用的落地流程。这篇东西不聊概念,只讲在真实业务里怎么把KMP用起来——模块怎么切、expect/actual怎么规避坑、网络层怎么优雅共享、构建速度怎么不拖垮CI,全是一线操盘时验证过的方案。
如果你正打算在团队里引入KMP,或者已经写了几个Demo但不知道怎么落到业务上,这篇文章应该能帮你省掉不少试错成本。
1. 为什么是KMP:跨平台方案选型的底层逻辑
1.1 三个主流方案的取舍对比
选KMP之前,团队大概率已经对比过Flutter和React Native。我自己的判断标准很简单:业务是否强依赖原生体验,以及团队现有技术栈能不能复用。Flutter在UI一致性上确实无解,一套Dart代码渲染所有端,但代价是脱离了Android和iOS的系统组件生态,凡是涉及系统级能力、深色模式联动、无障碍支持,都要额外做桥接。React Native思路类似,JS生态丰富,但性能瓶颈和依赖管理的老问题一直没根治。
KMP的定位完全不同。它不碰UI层,只共享业务逻辑——数据模型、网络请求、本地存储、账号登录这些和平台耦合度低但又最容易写重复的代码。Android端用Kotlin直接跑,iOS端编译成Framework给Swift调用,逻辑只有一份,UI各自用原生写。2025年的KMP体验相当顺滑,Android Studio直接支持多平台调试,Xcode里也能断点调试Kotlin代码,不再是早年那种写完逻辑还得靠日志看结果的狼狈状态。
1.2 KMP真正适合的业务场景
我要泼一盆冷水:不是所有项目都适合立刻上KMP。如果你维护的是一个纯内部工具型App,或者业务逻辑极薄、大部分时间都在写UI,KMP带来的收益有限,反而徒增构建复杂度。KMP最适合的场景,是业务逻辑足够厚实的App——电商、社交、金融、内容资讯这类,登录注册、商品列表、订单流程、消息推送这些横跨所有端的逻辑,能占到整个App代码量的四成以上。把这部分抽成共享模块,收益几乎是立竿见影的。
我在团队里推KMP的时候,第一个试点选的就是账号注册登录链路。原因很直接:逻辑复杂(密码校验、验证码、token刷新、多设备互踢)、各端UI却相对简单,是最典型的“逻辑重、UI轻”模块。跑通这条路之后,再逐步把网络层、缓存层、埋点层都下沉进去。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 工程结构设计:共享代码的边界应该画在哪里
2.1 模块划分的推荐实践
KMP项目最忌讳的就是一个共享模块搞定一切。我见过不少团队把整个业务逻辑塞进一个模块,几百个文件堆在一起,最后谁都动不了。正确的做法是按功能域拆模块。
以我的实际项目为例,工程里一共有五个共享模块:
shared-core:纯Kotlin代码,包含基础工具类、协程扩展、通用数据模型,不含任何平台API。shared-network:基于Ktor Client封装网络请求层,包含统一的拦截器、错误处理、Cookie管理。shared-database:封装SQLDelight的数据访问层。shared-domain:业务领域层,每个业务域一个子包——account、order、message这种层级。shared-remote-config:远程配置拉取与缓存。
模块之间依赖关系必须单向:shared-domain依赖shared-network和shared-database,shared-core被所有模块依赖,但shared-core不依赖任何人。这套结构的好处是,平台相关的expect声明只出现在shared-core的最低层,上层共享代码全是纯Kotlin,数据流清晰,测试也好写。
2.2 工厂模式与依赖注入的解耦思路
多平台环境下的依赖注入,我强烈建议别上重量级框架。Koin虽然从3.4版本开始支持KMP,但初始化时的平台差异处理确实让人头疼。我在第一个项目里用了Koin,后期调试难度比预期大,第二个项目直接改成了手写工厂模式。
kotlin复制// 统一的服务定位器,每个业务模块注册自己的工厂函数
object ServiceLocator {
private val factories = mutableMapOf<Class<*>, () -> Any>()
fun <T : Any> register(clazz: Class<T>, factory: () -> T) {
factories[clazz] = factory
}
@Suppress("UNCHECKED_CAST")
fun <T : Any> resolve(clazz: Class<T>): T {
return (factories[clazz]?.invoke() ?: error("Service ${clazz.simpleName} not registered")) as T
}
}
使用的时候在平台入口注册具体实现:
kotlin复制// Android Application类里
ServiceLocator.register(TokenStorage::
