这两年做移动端技术选型的朋友,大概率都被这几个词绕晕过:Kotlin、Compose、Flutter。很多人把它们摆在一起比,但比着比着就发现,其实比的不是同一个维度的东西。我自己的理解是,真正的跨平台未来之争,本质上是两套路线之争:一套以 Kotlin Multiplatform(下文简称 KMP)为底座,把业务逻辑和 UI 一起在 Kotlin 生态里统一掉,Jetpack Compose Multiplatform 是这套路线的 UI 输出层;另一套是 Flutter 自成体系,从语言到渲染引擎全都自己来。这两条路都锚定“一套代码,多端运行”,但它们的工程哲学、代码复用率、性能模型和生态演进方向完全不同。
这篇文章我想把这些差异掰开揉碎,聊清楚它们各自的优势、局限和未来走势。也顺便给正在纠结“下一个跨平台项目到底用什么”的朋友一个可落地的参考。内容涉及 UI 开发模式、渲染机制、动态化能力、工程化适配、招聘成本和长期生态风险,尽量做到既有理论分析,也有我在实际项目中的体感。
1. 先说清楚一个常被误解的前提
1.1 Kotlin 凭什么和 UI 框架放在一起比
很多对移动端了解不深的人会问:“Kotlin 不是语言吗?语言怎么和 Flutter 比?”这个疑问很正常,但放到今天的技术语境里,Kotlin 代表的早已不是单纯的 JVM 语言,而是一整套跨平台基础设施。KMP 允许你用 Kotlin 编写共享业务逻辑,编译成 Android 的字节码、iOS 的二进制、JavaScript 代码、甚至原生可执行文件。它与 Flutter 的竞争,实际上发生在两个层面:一是共享逻辑层面的运行时,二是 UI 层的实现方案。
Kotlin 生态的野心本来是:不取代 UI 层,而是让你尽量少写平台代码。在 KMP 早期,UI 部分必须用 SwiftUI、原生 Android View 或 Compose 分别实现,共享的只是 ViewModel、网络层、数据存储这类纯业务逻辑。后来 Compose Multiplatform 把 Jetpack Compose 渲染层带到了 iOS 和桌面端,Kotlin 路线的目标才变成“UI 也共享”。但要注意,Compose Multiplatform 到目前为止真正稳定且被广泛使用的还是 Android 和桌面端,iOS 端仍在快速迭代中,生产环境使用要谨慎评估。
所以真正要对比的其实是两个方案集合:Kotlin + Compose Multiplatform 的全共享路线,和 Flutter 的全共享路线。Kotlin 这半边是“我借用原生生态,能共享多少是多少”,Flutter 则是“从零开始建一个新生态,与原生生态保持隔离”。
1.2 Flutter 的“全包圆”哲学与底层代价
Flutter 和 Compose 有个表面相似处:都是声明式 UI。但没有几个人会认真追究,两者的渲染层实现完全不同。Flutter 自带 Skia(现在更流行 Impeller),对所有控件执行自绘,可以在 iOS 和 Android 上做到高度一致的渲染结果。但这种一致性和自绘框架也可能带来两个麻烦:一是包体积相对较大,二是与系统原生的深度集成能力有限制,比如某些系统级弹窗、键盘行为、无障碍能力,默认状态下总有那么一点“隔阂感”。
我做过一个 Flutter 的重度表单类应用,键盘弹出时的避让行为调了许久才接近原生效果。而 Compose Multiplatform 在 iOS 端依赖原生渲染管线,是把声明式 UI 映射到 UIKit 层面的控件,所以它在系统集成上先天更顺。当然,顺的代价是,同一套 Compose 代码,在不同平台上因为平台控件差异,视觉一致性不如 Flutter 自动统一。这就是两条路线在“一致性”和“平台性”之间各自选择的站位。
2. UI 开发模式与代码复用率对比
2.1 声明式 UI 的同与不同
Compose 和 Flutter 都是声明式 UI,这个概念很多人已经聊烂了,但实际写起来差异不小。Compose 用的是 Kotlin DSL,写 UI 就是写 Kotlin 函数,状态管理深度依赖 Kotlin 的协程和 Flow。Flutter 用的是 Dart 的 widget 树,一切皆 widget,状态管理用 Provider、Riverpod 或者 Bloc 等第三方库。
从工程整洁度上讲,Compose 因为天然带状(状态)在语言层,代码组织方式更贴近现代 Android 架构。Flutter 的 widget 嵌套抽象力很强,但项目一旦复杂,widget 树的层级往往比你想象中深,调试起来有一定的“地狱”风险。比如一个普通的列表页,Flutter 可能要包上 Scaffold、SafeArea、ListView、Padding、Container 等多层,而 Compose 虽然也有 Modifier 链,但逻辑上是扁平的。
实际编码手感上,Compose 写起来更像“用代码描述 UI”,Flutter 写起来更像“搭积木”。两种风格各有拥趸,但如果你已经是 Kotlin 开发者,Compose 的上手成本比 Flutter 低非常多。如果是前端转过来的,Dart 与组件树的概念迁移反而更容易。
2.2 业务逻辑层的复用策略与边界
UI 层只是表面问题,真正影响跨平台长期架构的是业务逻辑的复用策略。KMP 的思路是通过 expect/actual 机制暴露平台差异点,让网络、存储、领域模型、状态管理等模块做到“一次编写,处处运行”。你可以在共享代码里写一个数据仓库,Android 端直接调用,iOS 端也直接调用,只对数据库、文件路径、系统权限等少数差异点做 actual 实现。
Flutter 的做法是官方提供了一套统一的 API,网络、存储等直接走 Dart 生态,平台差异通过 MethodChannel 或 FFI 调用原生能力。二者的差别在于:KMP 在建模时就允许你为每个平台定制实现,而 Flutter 是先给你一个统一抽象,不够的时候再开洞。从代码复用率上限看,KMP 更高,因为它没有屏蔽平台能力;从下限安全看,Flutter 更稳,因为默认行为是统一的,不容易写出“Android 能跑 iOS 崩了”的代码。
我自己的经验是,KMP 项目里 expect/actual 的使用一定要克制。每多一个 actual,就多一份维护成本,平台差异的坑也会积累。Flutter 的方法通道同理,开洞越多,工程质量越难保证。未来所有跨平台框架的胜负手,不在于谁把统一抽象做得更漂亮,而在于谁能让“平台差异逻辑”的爆炸半径更小。
2.3 一个具体案例:表单页面的实现差异
拿一个常见的登录表单来对比。KMP + Compose Multiplatform 的写法大致是这样:
@Composable fun LoginScreen(viewModel: LoginViewModel) { val state by viewModel.uiState.collectAsState() Column(Modifier.padding(16.dp)) { OutlinedTextField( value = state.username, onValueChange = viewModel::onUsernameChange, label = { Text("用户名") } ) OutlinedTextField( value = state.password, onValueChange = viewModel::onPasswordChange, label = { Text("密码") } ) Button(onClick = viewModel::login, enabled = state.canSubmit) { Text("登录") } } }Flutter 的写法则是:
class LoginScreen extends StatelessWidget { final LoginViewModel viewModel; const LoginScreen({super.key, required this.viewModel}); @override Widget build(BuildContext context) { return Column( children: [ TextField( onChanged: viewModel.onUsernameChange, decoration: const InputDecoration(labelText: '用户名'), ), TextField( onChanged: viewModel.onPasswordChange, decoration: const InputDecoration(labelText: '密码'), ), ElevatedButton( onPressed: viewModel.login, child: const Text('登录'), ), ], ); } }从这段对比可以看出,两者在表达能力上接近,差异点主要在状态订阅方式和层级组织上。Compose 的 collectAsState 把状态订阅融入了语言层,Flutter 需要显式把 viewModel 传入组件树。哪种更好,取决于项目团队的习惯。但从维护角度,Compose 的状态收敛性更符合“数据驱动 UI”的直觉。
3. 性能与渲染机制的底层博弈
3.1 渲染管线的本质差异
这会聊到跨平台方案最核心的看点。Flutter 走的是一条“自绘渲染”路线:Dart 层生成层树,然后交给自带的渲染引擎直接绘制,不走系统控件。这样做的好处是渲染一致性与跨平台统一性都很好,坏处是每次绘制都需要经过自有管线,而自有管线的性能和系统控件的原生渲染相比,在某些低频场景会有细微的不放心。Impeller 引擎的引入就是为了解决 Skia 在 iOS 上因驱动编译导致的卡顿问题,这也是 Flutter 团队在性能上持续补课的真实写照。
Compose Multiplatform 在不同平台上的渲染逻辑不一样。在 Android 上,它走的是 Android 原生渲染管线,与系统 UI 体系同生共死;在 iOS 上,Compose Multiplatform 是通过 Skia 绘制到 UIViews 上,或者未来逐步过渡到更成熟的渲染层。这种“平台自定义”做法,理论上比 Flutter 更亲原生,但实现统一性的难度更高,需要在不同平台维护不同的渲染后端。
对我这种常年写客户端的人来说,性能指标不能只看评测数据,还要看在弱机上的表现。Flutter 在小内存低端机上表现不错,因为它的渲染引擎不依赖系统控件机制,启动后可以更早进入首帧绘制。Compose 在 Android 上因为有系统管线加持,配合协程做异步布局,在中高端机型上的流畅度非常好,但 iOS 端铺开后的性能,目前还需要更多生产级项目去验证。
3.2 包体积与启动时长,实际体感更重要
跨平台方案逃不掉包体积这个坎。Flutter 的最小应用包体积,通常比原生大出一截,原因在于带了一个渲染引擎和 Dart 运行时。Compose Multiplatform 的 Android 端因为大部分依赖系统 Jetpack 组件,增量相对可控,但 iOS 端引入 Compose 运行时后,增加幅度也不算小。
我一直认为,包体积和启动时长这类指标要在真实业务里看,而不是看 demo。纯计数器 demo 的包体积没有参考价值。一个真实 App 里,业务代码和依赖库的体积通常远大于框架本身的增量,所以框架层的 10MB 差异,放到 80MB 的业务包里,感知并不强。启动时长同理,真正的启动耗时大头在于网络请求初始化、首屏数据准备、路由表加载,这些跨平台方案之间的差异很小。
3.3 动态化与热更新的未来可能
动态化和热更新,是很多国内团队选型时极其看重的一点。Flutter 因为渲染引擎自绘,逻辑层与 UI 层都是 Dart 代码,具备一定的动态化改造基础。社区里也确实有人在做 Flutter 的云端推更新方案,但官方始终对热更新持保守态度,毕竟平台审核政策在那里摆着。
Compose Multiplatform 在 Android 上可以伴随原生热修方案,因为最终产物就是标准 Android 应用的字节码;iOS 端则因为系统机制限制,和原生应用一样不适合热更新。所以,在动态化这个指标上,Compose 和 Flutter 其实都算不上突出,反而是 React Native 类的 JS 方案更有优势。动态化能力要考虑,但不该成为跨平台框架选型的唯一决定性因素,毕竟商业应用如果过度依赖热更新,付出的架构代价和合规风险都不小。
4. 工程化、生态与团队选型现实
4.1 依赖生态的成熟度差距
现在看生态,Flutter 的第三方库数量和稳定性总体占优。pub.dev 上高质量的 UI 组件库、图表库、地图库已经非常丰富,而且因为是自绘渲染,这些库在 iOS 和 Android 上表现一致。Compose Multiplatform 的生态正在快速增长,但大部分库还停留在 Android 优先的阶段,真正同时支持 iOS 的并不多。如果你项目里有复杂图表、播放器、AR 之类的需求,Flutter 的现成方案更多。
不过这并不意味着 KMP 生态处于劣势。KMP 最独特的生态是“复用已有 JVM 生态”。Java/Kotlin 领域的库几乎可以直接拿来用。比如你用 KMP 做共享网络层,可以直接依赖 Retrofit 的 KMP 分支;做依赖注入,可以用 Kotlin 插件的 Koin 或 Ktor 客户端。这与 Flutter 必须“找到 Dart 版实现”有本质区别。对后端已经是 Kotlin/Java 的团队来说,KMP 可以顺带把客户端和后端的部分代码模型一起共享,这种生态联动效应是 Flutter 给不了的。
4.2 团队技能结构的隐性成本
这是选型时最容易被忽略、却最影响实际落地效果的部分。Flutter 团队要求成员掌握 Dart 和 widget 体系,如果团队全是 Android 出身,Dart 语法学起来不难,但 widget 组件体系和状态管理的思维转换需要时间。而 Compose 路线的团队基本就是原生 Android 技术栈的延伸,Kotlin 开发者几乎零基础就能上手 Compose,KMP 的 expect/actual 概念也是基于常规 JVM 开发习惯,学习曲线比 Flutter 平滑得多。
我见过不止一个团队,为了追 Flutter 的热度,从 Android 原生转过去,前半年都在为“怎么用 widget 组织业务”而挣扎。反观另一个团队,从 Compose 1.0 开始就在 Android 上积累全部 UI 代码,等到 Compose Multiplatform 支持 iOS 后,直接用自动化构建脚本把共享模块带过去,前后衔接顺畅很多。长期来看,Kotlin 生态的团队技能可迁移性更好,因为 Kotlin 也是后端、服务端等领域的主流语言。
4.3 招聘市场与职业安全度
从一个打工人视角看,Flutter 开发者的招聘需求在过去几年膨胀很快,但岗位上更多集中在某些特定业务类型。KMP 的招聘市场相对窄,因为真正跑 KMP 的公司还没有那么多。不过,如果你是一个资深 Android 工程师,懂 KMP 和 Compose Multiplatform,在招聘市场上属于“既有原生深度,又有跨平台广度”的高溢价类型。而 Flutter 岗位的竞争者数量更大,必须具备更深的工程能力才能脱颖而出。
4.4 我理解中的长期生态演进
聊未来,Kotlin 路线最大的底气来自决策者依然是 Google。虽然 Kotlin 基金会由多家公司参与治理,但 JetBrains 和 Google 在 Kotlin 上的投入是持续的,Compose Multiplatform 已经被提升为 Google 认可的跨平台方向。Flutter 的底气也来自 Google,但相比 Compose Multiplatform,Flutter 更像一个独立产品,和 Android 原生体系的协同没有那么深入。如果未来 Android 平台自身发生大的架构变化,Compose 路线会迅速跟着调整,而 Flutter 需要自行适配。
因此,我的判断是,未来两三年内,Flutter 在跨平台 UI 市场的份额优势依然存在,尤其在中小团队和简单业务场景,它的“开箱即用”特性极具吸引力。Compose Multiplatform 和 KMP 则会更受中大型客户端团队的青睐,因为他们的核心诉求是“在保持原生体验的前提下,降低多端维护成本”。
5. 关键指标速查与选型参考
5.1 对照表:一次性看透差异
下面这张表,是我在做内部技术调研时整理的,直接抄去可以用:
| 维度 | KMP + Compose Multiplatform | Flutter |
|---|---|---|
| 语言 | Kotlin | Dart |
| UI 构建方式 | 声明式 UI(Kotlin DSL) | 声明式 UI(Widget 树) |
| Android 端渲染 | 原生渲染管线 | 自绘渲染引擎(Skia/Impeller) |
| iOS 端渲染 | 原生映射/自有绘制(迭代中) | 自绘渲染引擎 |
| 代码共享范围 | 业务逻辑可达 100%,UI 逐步覆盖 | 业务与 UI 全共享 |
| 生态多样性 | 继承 JVM/Kotlin 生态,复用能力强 | Dart 专属生态,UI 库丰富 |
| 动态化能力 | 依赖原生方案,天然受限 | 官方保守,社区方案有限 |
| 团队转型成本 | 原生 Android 成本低 | 全新语言与框架,成本中等偏高 |
| 社区热度 | 快速上升期,iOS 案例渐增 | 成熟稳定,案例丰富 |
5.2 场景化选型建议
如果你们是中小型团队,业务以 UI 展示、电商、工具类为主,对原生能力和多端一致性的要求不是极致苛刻,Flutter 仍然是最省心的选择。它的文档、组件、插件都成熟,团队招人也相对容易。
如果你们是中大型团队,有独立的 Android 和 iOS 原生团队,并且希望在未来减少重复开发,KMP 是更稳健的演进方向。先共享网络层和业务逻辑层,UI 继续用原生实现,等 Compose Multiplatform 在 iOS 端更成熟后再逐步共享 UI,这是一条低风险的过渡路径。
如果你们是个体开发者,想在多个平台发布小工具或内容型应用,并且对 Kotlin 有一定熟悉度,其实完全可以先尝试 Compose Multiplatform 的桌面端和 Android 端,因为它可以少搭一套前端框架。不过要有心理准备,iOS 端的坑可能比 Flutter 多一些,需要耐心打磨。
6. 实战经验与踩坑记录
6.1 我在 KMP 共享模块中犯过的错
第一次把 KMP 模块接入真实项目时,我对 expect/actual 的粒度把握太粗,把整个网络层设计成了完全共享。结果发现不同平台的 TLS 证书校验逻辑、Cookie 管理策略差异很大,为了统一抽象,我把大量平台参数传进共享层,接口变得异常臃肿。后来改成“共享业务编排逻辑,网络协议栈保持平台独立”,问题才缓解。这个案例说明,KMP 的共享边界最好按“业务用例”划分,而不是按“技术组件”划分。
6.2 Flutter 高性能列表的隐性瓶颈
另一个项目里,我用 Flutter 写了一个大数据量列表。初版把所有行组件都包在了一个大 Column 里,滑动流畅度还行,但一旦列表项内嵌图片和文本混排,滑动就会出现可感知的掉帧。后来换成 ListView.builder 并配合 const 构造和 itemExtent 指定固定高度,流畅度才恢复。这类问题在 Compose 里对应的是 LazyColumn 的 key 与 item 内容稳定性,需要提前做好规划。
6.3 团队实验的最终收获
做过一轮团队验证后,你会得到两个直接的感受。一是,选型这事没有银弹,只有适合不适合;二是,跨平台框架的未来,不只是技术能力竞争,还是生态协同能力竞争。Kotlin 阵营与原生生态同根,Flutter 阵营自带闭环,两条路都会成熟,但成熟的方向和应用边界会越来越分明。
7. 下一步你可以怎么动手验证
如果你的团队还在犹豫,我建议不要只停留在资料对比,而是用一个真实业务小功能做 PoC。具体可以分三步走:先选定一个低风险业务,比如登录注册模块;然后分别用 Compose Multiplatform 和 Flutter 实现同一个功能;最后让开发和测试分别记录开发效率、工具链体验和系统适配工作量。用一两周的数据,比看一百篇文章都有说服力。
我个人在实际操作中的体会是,如果你手头已经有一个成熟的 Android 技术团队,跨平台的第一步最好不要选择 Flutter 这种全切换方案,KMP 的渐进式演进在组织内部推进遇到的阻力会小很多。如果你的团队本来就是杂牌军,Java、JS、C++ 什么都有,Flutter 的统一性可能会让你的工程管理更顺畅。
再分享一个小心得:做跨平台选型决策时,一定要把“未来两年的业务方向”这个变量放进去。如果业务高度依赖地图、音视频、硬件交互,任何纯跨平台方案都要慎重。反之,如果业务以内容消费和表单展示为主,不用犹豫,Flutter 或者 Compose Multiplatform 都能很好满足你。
移动端跨平台的未来,大概率不会出现一家独大。Compose Multiplatform 和 KMP 会在原生体验敏感的业务里持续渗透,Flutter 会在追求开发效率与多端一致性的产品里保持强势。真正值得关注的,不是哪条路线会赢,而是你自己所在的团队更适合哪条路线,以及你能否让团队的核心能力沉淀在这条路线上,形成长期壁垒。