别再说 Flutter 是唯一选择了——KMP 正在悄悄抢走它的地盘

800 阅读8分钟

🤔 一个反常识的问题

如果我问你"2024 年之后跨端移动开发的主流方案是什么",十个人里有八个会说 Flutter。毕竟 Google 背书、生态成熟、UI 一致性强,这几乎已经是行业共识。

但我最近遇到一件事让我重新想了想这个问题:一位在大厂做 Android 的朋友,主导把团队的某个核心模块迁到了 KMP(Kotlin Multiplatform Mobile),不是因为 Flutter 不够好,而是因为他说了一句话——

"我不想用另一套语言描述我的业务逻辑。"

这句话戳到我了。Flutter 你写的是 Dart,KMP 你写的是 Kotlin——而 Android 工程师本来就在写 Kotlin。这不只是语言熟悉度的问题,更是一个关于"代码归属感"和"架构融合度"的深层选择。

今天这篇文章,我想从一个 Android 工程师的视角,聊聊 KMP 在真实项目中的落地体验,以及它和 Flutter 之间那些真正重要的差异——不是官方文档上的那种,而是干活时你会遇到的那种。

🧩 KMP 是什么:先搞清楚它在解决什么问题

很多人第一次接触 KMP 会有一个误解:以为它是 Flutter 的竞争对手,能做到"一套代码跑全端 UI"。其实不是,至少不完全是。

KMP 的核心定位是:共享业务逻辑,UI 各平台自己来

它的架构分层很清晰:

层次AndroidiOS共享?
UI 层Jetpack Compose / XMLSwiftUI / UIKit❌(各自实现)
ViewModel / PresenterKotlin ViewModelKotlin ViewModel(通过 KMP)✅ 可以共享
业务逻辑 / 领域层Kotlin 编写,完全共享✅ 完全共享
数据层(网络/本地存储)Ktor + SQLDelight 跨平台✅ 完全共享

换句话说,KMP 让你把那些真正费脑子的部分——业务规则、数据处理、状态管理——写一次,然后双端复用。UI 嘛,Android 继续用 Compose,iOS 继续用 SwiftUI,各自顺滑,互不干扰。

而 Compose Multiplatform 是 KMP 的 UI 扩展,让你可以选择把 Compose UI 也共享到 iOS 端。这是个可选项,不是强制的。

🔧 实战落地:一个真实的模块迁移案例

我们来看一个实际场景:把一个"用户积分计算模块"从 Android 原生迁移到 KMP 共享模块,同时对接 iOS 端。

第一步:创建 KMP 共享模块

项目结构大概长这样:

shared/
├── commonMain/
│   └── kotlin/
│       └── com/example/points/
│           ├── PointsRepository.kt    # 共享业务逻辑
│           ├── PointsCalculator.kt    # 核心计算
│           └── model/
│               └── UserPoints.kt     # 数据模型
├── androidMain/
│   └── kotlin/
│       └── com/example/points/
│           └── AndroidPointsDataSource.kt  # Android 特有实现
└── iosMain/
    └── kotlin/
        └── com/example/points/
            └── IosPointsDataSource.kt      # iOS 特有实现

第二步:在 commonMain 写共享业务逻辑

// commonMain - PointsCalculator.kt
class PointsCalculator {
    
    fun calculate(
        basePoints: Int,
        multiplier: Double,
        bonusEvents: List
    ): PointsResult {
        val base = (basePoints * multiplier).toInt()
        val bonus = bonusEvents.sumOf { it.points }
        val total = base + bonus
        
        return PointsResult(
            basePoints = base,
            bonusPoints = bonus,
            totalPoints = total,
            level = determineLevel(total)
        )
    }
    
    private fun determineLevel(points: Int): UserLevel = when {
        points >= 10000 -> UserLevel.PLATINUM
        points >= 5000  -> UserLevel.GOLD
        points >= 1000  -> UserLevel.SILVER
        else            -> UserLevel.BRONZE
    }
}

data class PointsResult(
    val basePoints: Int,
    val bonusPoints: Int,
    val totalPoints: Int,
    val level: UserLevel
)

enum class UserLevel { BRONZE, SILVER, GOLD, PLATINUM }

第三步:处理平台差异——expect/actual 机制

KMP 最核心的跨平台机制就是 expect/actual。在 commonMain 里声明接口期望,各平台提供实现:

// commonMain - Platform.kt
expect class PlatformContext

expect fun getPlatformName(): String

expect fun getCurrentTimeMillis(): Long
// androidMain - Platform.android.kt
actual class PlatformContext(val context: Context)

actual fun getPlatformName(): String = "Android ${Build.VERSION.SDK_INT}"

actual fun getCurrentTimeMillis(): Long = System.currentTimeMillis()
// iosMain - Platform.ios.kt
actual class PlatformContext

actual fun getPlatformName(): String = UIDevice.currentDevice.systemName() + 
    " " + UIDevice.currentDevice.systemVersion

actual fun getCurrentTimeMillis(): Long = 
    (NSDate().timeIntervalSince1970 * 1000).toLong()

第四步:网络请求用 Ktor

// commonMain - PointsApiClient.kt
class PointsApiClient {
    private val client = HttpClient {
        install(ContentNegotiation) {
            json(Json { ignoreUnknownKeys = true })
        }
        install(HttpTimeout) {
            requestTimeoutMillis = 10_000
        }
    }
    
    suspend fun fetchUserPoints(userId: String): UserPoints {
        return client.get("https://api.example.com/points/$userId").body()
    }
    
    suspend fun syncPoints(userId: String, points: PointsResult): Boolean {
        val response = client.post("https://api.example.com/points/sync") {
            contentType(ContentType.Application.Json)
            setBody(SyncRequest(userId, points.totalPoints))
        }
        return response.status.isSuccess()
    }
}

注意:这段代码在 Android 和 iOS 上完全一样,不需要任何修改。Ktor 底层会自动选择对应平台的 HTTP 引擎(Android 用 OkHttp,iOS 用 NSURLSession)。

第五步:iOS 端调用——Kotlin 编译成 Framework

KMP 会把共享模块编译成一个 .xcframework,iOS 侧直接 import 使用:

// iOS Swift 代码
import shared  // 导入 KMP 编译的 Framework

class PointsViewController: UIViewController {
    
    private let calculator = PointsCalculator()
    private let apiClient = PointsApiClient()
    
    func loadPoints(userId: String) {
        // 直接调用 Kotlin 编写的共享代码
        Task {
            let userPoints = try await apiClient.fetchUserPoints(userId: userId)
            let result = calculator.calculate(
                basePoints: Int32(userPoints.base),
                multiplier: userPoints.multiplier,
                bonusEvents: userPoints.events
            )
            await updateUI(result: result)
        }
    }
}

iOS 工程师看到这段代码的第一反应通常是:这跟调普通 Swift 库没什么区别。这正是 KMP 的设计目标——对 iOS 侧几乎透明。

⚔️ KMP vs Flutter:真正的核心差异在哪

聊了这么多 KMP 的实现,现在到了最关键的问题:我到底该选哪个

先上对比表,然后逐条说我的看法:

维度KMPFlutter
语言Kotlin(Android 原生语言)Dart(专为 Flutter 设计)
UI 共享可选(Compose Multiplatform)强制(所有 UI 用 Flutter)
原生体验极高(UI 完全原生)中等(自绘引擎,非原生组件)
iOS 集成编译为 Framework,无缝集成需要 FlutterEngine,有额外开销
渐进式迁移非常友好(模块级别替换)困难(通常需要整体重写)
团队要求Android 工程师上手快,iOS 需要 Kotlin 学习全端都需要学 Dart
生态成熟度快速成长,部分库仍在完善非常成熟,pub.dev 库极丰富
调试工具链Android Studio,IDE 支持一流VS Code + DevTools,体验良好
热重载Android 侧有,iOS 侧弱全端支持,体验极佳
公司背景JetBrains 主导Google 主导

光看表格可能还是模糊,我来说几个更有感触的点:

差异一:KMP 不强迫你放弃原生 UI

这是我认为 KMP 最被低估的优势。Flutter 的思路是"我来接管所有 UI",它自己画每一个像素,所以在某些平台上会有"不够原生"的观感——比如 iOS 上 Flutter 的 Material 组件默认不跟 iOS Human Interface Guidelines 走。

KMP 的思路恰恰相反:UI 是你的,逻辑是共享的。你的 iOS 用 SwiftUI,你的 Android 用 Compose,两边都是原生渲染,原生体验。这对于有严苛 UI 质量要求的 App(比如金融、医疗类)非常重要。

差异二:渐进式迁移是 KMP 的杀手锏

现实中很少有团队能从零起步,大多数面临的是"我有个跑了 3 年的 App,怎么引入跨端"。Flutter 基本上是个全或无的选择,混合接入 FlutterEngine 的成本相当高,性能也存在损耗。

KMP 天然支持渐进式:你可以先把网络层迁到 KMP 共享,其他不动;下个季度再把业务逻辑层迁过来;UI 层爱动不动。每一步都是可独立验证的小步骤,风险极低。

差异三:iOS 侧的开发者体验差距正在缩小

KMP 早期被诟病的一点是 iOS 侧体验差——协程暴露给 Swift 的方式比较别扭,编译时间长,错误提示不够友好。但这两年改善明显:

• SKIE(Swift Kotlin Interface Enhancer)让协程、Flow 等可以直接映射到 Swift 的 async/await 和 AsyncSequence

• 编译产物从 .framework 升级为 .xcframework,多架构支持更好

• Xcode 插件开始成熟,KMP 代码在 Xcode 里也能跳转、提示

当然,Flutter 的热重载体验依然是 KMP 短时间内很难超越的——写 UI 时秒级预览,开发效率高很多。

🎯 选型建议:什么团队该选什么

我的选型框架是这样的,可以按顺序问自己三个问题:

问题一:你的团队主力是 Android 工程师还是全栈/前端工程师

如果主力是 Android,KMP 的学习成本几乎为零,Kotlin 就是你们的日常语言;如果主力是前端/全栈,Flutter+Dart 的体系更系统,学习曲线更平滑。

问题二:你的 App 对原生 UI 交互要求高不高

金融 App、医疗 App、与系统深度集成的工具类 App,需要原生交互体验的,优先 KMP;游戏类、内容展示类、UI 高度定制化的,Flutter 的自绘引擎反而是优势。

问题三:你是新项目还是老项目改造

新项目两者都可以,生态成熟度上 Flutter 略占优;老项目改造,KMP 的渐进式迁移能力是明显优势,Flutter 几乎必须重写。

💡 实际落地建议:对于大多数有 Android 背景的团队,我推荐先用 KMP 共享数据层和业务逻辑层(网络、存储、领域模型),UI 层暂时不动。这个改造成本极低,收益立竿见影——iOS 和 Android 的业务 bug 从此只需要修一次。

🚧 KMP 的局限:别被我说得太美好

当然,KMP 不是银弹,有几个真实的坑需要说:

编译速度:KMP 项目的增量编译相比纯 Android 项目慢,iOS 端的 framework 编译尤其耗时,CI/CD 需要做好缓存策略

多线程模型:iOS 侧的 Kotlin/Native 对多线程有一些限制(Frozen Objects 问题),虽然新版本有所改善,但如果你的共享代码涉及复杂并发逻辑,需要额外注意

三方库生态:不是所有 Android 常用库都有 KMP 版本,比如 Room 的 KMP 版本(Room 2.7+)还相对较新,SQLDelight 是目前更稳妥的选择

iOS 工程师接受度:有些 iOS 工程师对"我的 App 里跑着 Kotlin 代码"这件事心理上有抵触。这不是技术问题,是团队文化问题,需要提前沟通

⚠️ 特别提醒:不要在刚开始就尝试用 Compose Multiplatform 共享 UI 到 iOS,这个特性虽然已经稳定,但生态和工具链还在成长中,坑比较多。先从共享业务逻辑层开始,是最稳妥的路径。

📝 最后说一句

Flutter 和 KMP 不是非此即彼的关系,它们解决的是不同层次的问题。Flutter 更擅长"从零开始构建跨平台 UI",KMP 更擅长"让已有的 Android 逻辑平滑复用到 iOS"。

如果你是 Android 工程师,我真心建议你花一个下午把 KMP 官方的 Getting Started 跑一遍。那种把一段 Kotlin 代码同时跑在 Android 模拟器和 iOS 模拟器上的感觉,还挺上头的。

不一定要在项目里立刻用,但心里有这张牌,下次选型的时候你会更有底气。


本文为原创技术文章,欢迎转载请注明出处。如有问题或建议,欢迎在评论区留言交流。