第111篇 Kotlin Multiplatform 初识:跨端共享业务逻辑

简介: KMP是JetBrains推出的跨平台框架,核心价值在于共享业务逻辑(domain/data层),而非UI。通过`expect/actual`机制,同一份Kotlin代码可编译至Android、iOS、桌面、Web等平台,提升复用效率,降低维护成本。

前面几节都在 Android / JVM 范围内,这一节把视野打开到 Kotlin Multiplatform(KMP)。这是近年 Android 面试里的新兴高频题——不是因为它有多难,而是因为很多人只知道"跨平台"四个字,答不出边界在哪里、什么时候不该用。这一节的判据很清楚:KMP 的价值在"共享业务逻辑",不在"共享 UI"。

先把结论放在前面:KMP 的核心能力是"同一份 Kotlin 代码编译到多个平台"。工程上真正划算的共享范围是 domain 层与 data 层(模型、网络、存储、算法),UI 层仍由各端原生实现(Android 用 Compose/View,iOS 用 SwiftUI/UIKit)。原因是各端 UI 体系与交互规范差异太大,硬共享 UI 会让 expect/actual 失控,反而拖慢迭代。

机制背后的执行路径

KMP 的技术底座是 expect / actual 这对关键字:

// commonMain:声明"这里需要一个平台实现"
expect class PlatformClock {
   
    fun nowMillis(): Long
    fun locale(): String
}

// androidMain:Android 侧真实实现
actual class PlatformClock {
   
    actual fun nowMillis() = System.currentTimeMillis()
    actual fun locale() = Locale.getDefault().toLanguageTag()
}

// iosMain:iOS 侧真实实现(Kotlin/Native,Objective-C 互操作)
actual class PlatformClock {
   
    actual fun nowMillis() = NSDate().timeIntervalSince1970.toLong() * 1000
    actual fun locale() = NSLocale.currentLocale.languageCode
}

编译时,编译器把 common 里的 expect 声明与各端的 actual 实现绑定。这条机制是理解 KMP 一切取舍的钥匙:每一个 expect 都要在每个目标平台各写一份 actual,所以它的成本是"平台数 × 抽象数量"。

KMP 目标平台演进路线也值得知道:早期是 JVM + JS + Native(Native 成熟后 Android 桌面端共用),后来 Kotlin/Wasm 加入。commonMain 可以按源集再细分:commonMain(全平台共享)、androidMain(JVM 系共享,含 Android + 桌面)、nativeMain(所有原生共享)、iosMain(iOS 专属)。这个层次结构直接决定了代码该写在哪一层。

真实工程场景的推演

场景一:网络层共享。Retrofit/OkHttp 是 JVM 库,Android 直接用;iOS 侧要用 Ktor(Kotlin/Native 有实现)。于是接口定义与响应模型放 common 层,HTTP 客户端用 expect 注入:

// commonMain
interface UserApi {
    suspend fun fetch(id: String): User }

expect class HttpClientFactory {
   
    fun create(): HttpEngine
}

代价是:接口定义要避开 JVM 特有类型(不能用 java.io.File、不能用 CompletableFuture),否则 iOS 侧无法实现。这是 KMP 最常见的"写着写着才发现不通用"的时刻。

场景二:存储层共享。Room 是 Android 专属,iOS 用 Core Data/SQLite。两个都写一遍 ORM 成本极高,通常做法是共享数据模型与查询逻辑的定义,平台侧各自实现。KMP 现在有 KMP 版的 Room 支持多端,把这一块简化了不少。

场景三:应该拒绝共享的部分。导航、权限、UI 组件——各端交互规范不同(Android 的返回键、iOS 的手势、桌面端的窗口系统),共享后要么 expect 爆炸,要么做成"四不像"。这是"会复盘的人答得好"的原因:能说清哪些不该共享,比能列共享清单更有价值。

场景四:收益要算账。KMP 适合"多端发布 + 业务逻辑稳定 + 团队有 Kotlin 基础"。若只有 Android 一个端、或业务还在快速试错、或两端 UI 差异极大,KMP 的抽象成本大于收益。

最常见的坑是

把平台 UI 逻辑塞进共享层,expect/actual 失控反而拖慢迭代。 根因是把"能共享"当成"应该共享"。修法:划定共享边界——common 只放 data 与 domain,ui 各端独立;每个新增 expect 都要问"iOS 与桌面端各自实现会不会比写两遍更麻烦"。

其次是在 common 层用了 JVM 特有 API(如 java.util.Date、java.io.File、Regex 的部分重载)。修法:common 层严格依赖 Kotlin 标准库与 KMP 版本库(kotlinx-datetime、kotlinx-io),配 IDE 提示确保没有 JVM-only import。

还有一个更隐蔽的坑:iOS 侧 Objective-C 互操作的桥接成本被低估。Kotlin/Native 生成的类型要在 Swift 侧可用,需要处理可空性、集合类型映射、密封类暴露等问题,前期投入不小。修法:共享层返回"扁平"的简单类型(基本类型、String、List),复杂对象在平台侧再包一层。

现场手写这一段就够了

// commonMain:只依赖 Kotlin 标准库与 KMP 库
expect class Log {
   
    fun d(tag: String, msg: String)
    fun i(tag: String, msg: String)
}

expect fun platformName(): String
expect class Files private constructor() {
   
    fun readText(path: String): String
    fun writeText(path: String, text: String)
    companion object {
    fun default(): Files }
}

// commonMain:业务层完全共享,零 expect
data class User(val id: String, val name: String, val age: Int)

class UserRepository(private val api: UserApi, private val log: Log) {
   
    suspend fun profile(id: String): Result<User> = runCatching {
   
        log.d("UserRepo", "fetch $id")
        api.fetch(id).also {
    log.i("UserRepo", "ok ${it.id}") }
    }
}

// androidMain:JVM 实现
actual class Log {
   
    actual fun d(tag: String, msg: String) = Log.d(tag, msg)
    actual fun i(tag: String, msg: String) = Log.i(tag, msg)
}
actual fun platformName() = "Android"
actual class Files private constructor() {
   
    actual fun readText(path: String) = java.io.File(path).readText()
    actual fun writeText(path: String, text: String) = java.io.File(path).writeText(text)
    actual companion object {
    actual fun default() = Files() }
}

// iosMain:Kotlin/Native 实现
actual class Log {
   
    actual fun d(tag: String, msg: String) = NSLog("D/%@: %@", tag, msg)
    actual fun i(tag: String, msg: String) = NSLog("I/%@: %@", tag, msg)
}
actual fun platformName() = "iOS"
actual class Files private constructor() {
   
    actual fun readText(path: String) = NSString.stringWithContentsOfFile(path, encoding = NSUTF8StringEncodingEncoding, error = null).toString()
    actual fun writeText(path: String, text: String) {
    text.writeToFile(path, atomically = true, encoding = NSUTF8StringEncoding, error = null) }
    actual companion object {
    actual fun default() = Files() }
}

// 源集层次:commonMain → androidMain / nativeMain → iosMain
// 依赖声明(build.gradle.kts 片段)
// commonMain deps: kotlinx-coroutines-core, kotlinx-datetime
// androidMain deps: retrofit, okhttp
// iosMain deps: ktor-client-darwin

关键行解读:expect 只在 common 声明契约;actual 在各平台实现,必须都实现否则编译不过——这正是约束边界的机制;UserRepository 里零 expect,说明业务层完全可共享;iOS 侧返回扁平类型降低 Swift 桥接成本;源集层次决定"能放什么代码"。

面试追问四连

"KMP 适合共享哪些代码?" 答:模型、算法、网络客户端抽象、本地存储、纯业务规则。UI、导航、权限各端独立。判据是"两端实现是否比共享更麻烦"。

"expect/actual 的代价是什么?" 答:每个 expect 要在每个目标平台各写一份 actual,成本是"平台数 × 抽象数";抽象会阻碍两端的差异化优化,平台特有的最佳实践无法直接落地。

"KMP 和 React Native / Flutter 有什么不同?" 答:RN/Flutter 共享 UI 层(自绘或同构组件),KMP 共享业务层而 UI 各端原生。KMP 的产物体积与性能更接近原生,UI 一致性成本更高;RN/Flutter 反之。

"为什么 iOS 侧成本比 Android 高?" 答:Kotlin/Native 与 Objective-C/Swift 互操作需要处理可空性标注、集合映射、密封类暴露等问题,且无法直接用 iOS 生态的成熟库(要么等 KMP 移植,要么走 actual 封装)。

落地建议

1. 先划边界再写代码:在项目启动时定好"common 只放 data/domain",UI 各端独立,并写进架构文档;每个新增 expect 走一次边界评估。 2. common 层严格限制依赖:只用 Kotlin 标准库与 KMP 版本库;配 IDE 检查禁止 java.* import 出现在 commonMain。 3. iOS 侧返回扁平简单类型(基本类型、String、List、Map),复杂模型在平台侧包一层,降低 Swift 互操作成本。 4. 自动化预防:为 common 层写纯 JVM 单测(可跑在 JVM 目标上,成本最低),业务规则用这套测试保证跨端一致;平台特有逻辑各端单独写测试。

给正在准备面试的你 把这题画成"分层共享图"。从上到下四层:最上层"UI 层"分成左右两个框(Android: Compose/View ‖ iOS: SwiftUI/UIKit),中间用虚线隔开,标"各端原生,不共享";下面"domain 层"是一个横贯的大框(模型、业务规则、UseCase),标"✅ 全量共享";再下面"data 层"横贯大框(网络抽象、存储抽象),标"✅ 共享接口,实现用 expect/actual";最底层"platform 层"分成 Android(JVM)与 iOS(Native)两个实心框,标"平台实现"。图右侧竖排写"expect 成本 = 平台数 × 抽象数"。

再补一个高分追问答案:"什么情况下 KMP 是错的选择?" 四种:①只有单一端发布,共享没有收益只有抽象成本;②业务仍在快速试错,抽象会拖慢变化;③两端 UI 与交互差异极大(如桌面端与移动端),共享 UI 反成拖累;④团队没有 Kotlin 基础,KMP 的编译配置与心智成本高于收益。能主动说出"这个方案不适用"的场景,通常比只会夸方案更能赢得信任。

复习时别孤立刷题:Flow 与 RxJava 对比——上一节做的是"同一问题的两种库选型",本节做的是"同一份代码的两种平台落地",都在训练技术选型的判断力。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:Flow-与-RxJava-对比:响应式迁移指南

下一篇预告:Kotlin-2.x-演进:K2-编译器带来了什么

有任何问题欢迎在评论区留言交流。

相关文章
|
2月前
|
缓存 安全 物联网
资产调拨完成,台账为何仍会失真?RFID资产异动闭环的5个校验点
本文剖析固定资产调拨中“单据完成≠状态同步”的痛点,提出覆盖申请、审批、交接、回写、验证的5个关键校验点,强调跨环节一致性与闭环管理。通过规则接口、乐观锁、RFID比对、事务控制及自动巡检等落地思路,助力企业实现实物、责任人、位置与台账“四一致”。(239字)
|
1月前
|
Java 编译器 测试技术
第114篇 函数式编程思维:纯函数与副作用管理
本文是Kotlin函数式编程思维的收官总结,揭示其核心:**用不可变数据 + 纯函数 + 显式副作用组织代码**。提炼三条可落地纪律——①默认`val`、状态变更即生成新快照;②副作用(IO/UI/日志)推至边界层(ViewModel/Composable);③用不可变数据流替代可变共享。强调FP本质是工程纪律,非教条,重在提升可测性、可推理性与协作清晰度。
68 2
|
1月前
|
安全 Java 定位技术
第117篇 Kotlin 阶段面试通关地图:高频题怎么连成网
这是Kotlin面试的结构化复习地图,非知识点罗列,而是将60+篇内容按「六大知识块×三层深度」(会用/懂机制/能取舍)分层归因,直击面试区分度核心。助你快速定位保底分与拉分点,掌握高频题答题骨架、交叉跳转逻辑及三大避坑指南,真正把知识织成可检索、可推演、可落地的活网络。
110 1
|
1月前
|
人工智能 自然语言处理 数据挖掘
QwenWork 深度解析:交付型企业 AI 智能体,基于 Qwen3.8 重构企业办公自动化流程
千问办公QwenWork打破传统大模型工具只做对话问答的边界。以Qwen3.8超大参数基座作为底层能力支撑,依托钉钉深度协同,构建起“沟通‑分析‑创作‑交付‑自动化”完整企业工作闭环。面向中小企业、职场个人,一套平台覆盖文档生成、网页产出、多模态解析、经营数据分析,同时实现企业AI资源管控与业务资产沉淀。对于想要推进内部办公数字化提效的团队,不用再采购多款独立工具,通过QwenWork可以把大量重复性文案、报表、简单开发类工作交给Agent完成;底层兼容OpenAI协议,开发者也可以调用Qwen3.8系列模型,基于API进一步做二次开发,搭建企业内部定制化办公自动化流程。
156 1
|
1月前
|
存储 人工智能 运维
阿里云轻量应用服务器是什么?轻量指南、优势、支持镜像、收费标准及使用限制说明
阿里云轻量应用服务器是面向建站、开发测试及小型AI应用的轻量级云服务器,支持一键部署、应用镜像(如宝塔、Dify)、HTTPS和Web SSH,2核2G起仅38元/年起,操作简单、成本透明、开箱即用。(239字)
|
12天前
|
存储 弹性计算 人工智能
2026 年度阿里云双 11 活动解析:云服务器定价机制、资费一览表及采购选型参考
2026年的阿里云ECS云服务器双11,不再是简单的"降价促销",而是一场围绕‌用户分层、权益精准、长期锁定‌的系统性让利。38元的轻量秒杀是入门的钥匙,99元和199元的续费同价是长期的锚点,GPU折扣是AI时代的加速器,企业专属权益则是规模化上云的助推器。 ‌选对阿里云ECS云服务器,不只是省下一笔钱,更是为你的业务找到一个稳定、可预期、可扩展的算力基座。‌ 趁双11大幕正盛,先领券、再询价、后下单——让每一分预算,都在阿里云ECS云服务器上开出最绚烂的花。
153 2
|
14天前
|
人工智能
Markdown 样式设计:让内容拥有更好的表达力
本文探讨Markdown样式设计的核心价值:不止于美化,更在于提升信息的理解力、记忆性与传播力。涵盖极简、杂志、科技、毛玻璃等主流风格,强调内容与场景匹配,并提供选型方法论——兼顾阅读目的、内容类型与使用环境。(239字)
|
1月前
|
安全 数据安全/隐私保护 C++
【FHE 同态加密】我们如何实现同态加密推理(六):Key-Switch 重线性化——一个数量级(2^73 噪声)决定整个设计
本文深入解析CKKS同态加密中Key-Switch(重线性化)的核心设计逻辑:关键在于噪声数量级——未分解时引入约2⁷³噪声,远超60-bit模数容限;采用base-2²⁰数字分解降至2⁴⁵,方保安全。由此决定relin密钥需3×2个多项式,并延伸至Galois旋转密钥结构。(239字)
|
3月前
|
人工智能 监控 安全
高校校园综合安全防护体系构建研究
本文以宾州州立大学哈里斯堡分校安全指引为样本,系统剖析高校校园安全八大维度,提出“事前预防—事中响应—事后支持”全链条闭环治理框架,强调制度、技术、培训与文化协同,直面资源约束、开放性与行为不确定性等现实挑战。(239字)
152 4
|
1月前
|
JavaScript 算法 Java
第120篇 tailrec 与尾递归:递归优化的编译期支持
`tailrec` 是 Kotlin 的编译期优化机制:当函数满足尾调用条件时,编译器将其重写为循环,避免栈溢出。它不依赖 JVM(JVM 无 TCO),而是 Kotlin 自主实现;不支持 `suspend`、非尾位置调用或需回溯的场景。本质是“递归定义 → 累加器循环”的转换。
81 2

热门文章

最新文章