# 全栈 KMP **全栈**指:同时完成后端服务器的部分和前端用户的部分。 因而,在学习本节之前,最好先看 [Kotlin Ktor](./Kotlin-Ktor.md) 或 [Kotlin Springboot](./Kotlin-Springboot.md) 的其中一个。如果你想使用 KMP 最全面的功能,最好是选择 Ktor。 ## 一个完整的 KMP 项目 目前(2026.10.09),完整的 KMP 项目包含以下几项: - app - androidApp - jvmApp - iosApp - webApp - shared - commonMain - core - commonMain - server 除了 iOS 部分必须由 Xcode 做额外设置后编译以外,其他的应用均可在 JDK 下完成编译(Android 需要相应 SDK,其他需要编译为字节码的需要其他编译器)。 Server 本身也是运行在 JVM 上面的,它和普通的 Java / Kotlin 后端项目基本没有区别,除非使用特殊的 Ktor 后端编译为字节码。不过,在 KMP 中,将它的部分代码写在 `core/commonMain` 中,可以在一定程度上减少代码的出错可能,还可以进行代码复用。 我们在编写全栈 KMP 时,应当优先考虑 Web(WASM) 和 Server 端能否完美运行。 在默认状况下,`core` 的依赖关系在 `shared` 之上;`server` 与整个 `app` 模块平级,在 `core` 之下;`app` 内部模块在 `shared` 之下。不建议自行改动。 ## 整体的设计思路 既然 KMP 最大的特点是多平台代码共享,那么这里就要考虑到把 后端的 entity(vo, dto),网络接口(如果后端框架支持拆开写的话)等一并写在共享代码的部分,即 `core/commonMain` 内部。 ## RPC 网络交互 在全栈开发中,前端与后端的通信是不可避免的一环。传统的开发流程中,前后端往往通过 RESTful API(HTTP + JSON)进行通信,但全栈 KMP 提供了一种更优雅、类型更安全的通信模式——**RPC(Remote Procedure Call,远程过程调用)**。 一般来讲,在域名后面,传统网络 api 以 `/api` 开头,websocket 服务以 `/ws` 开头,PRC 服务以 `/rpc` 开头。 ### 为什么在全栈 KMP 中推荐 RPC? 在传统的双端分离架构(如前端 TS/Vue/React + 后端 Java/Go)中: - 后端编写 Controller 暴露 HTTP 路由与参数。 - 前端需要查阅 Swagger / API 文档,手动拼写 URL、路径参数与请求体。 - **痛点**:字段名称变更、类型变动无法在编译期间发现,一旦接口不一致只能在运行时报错。 而在**全栈 KMP**体系下,前端(Android、iOS、Desktop、Web)和后端(Ktor Server)全部使用 Kotlin 编写: 1. **如同调用本地函数**:前端通过网络调用后端服务,就像调用本地对象的 `suspend` 函数一样简单自然,免去手动处理 URL 路径、请求方法和 HTTP 状态码的烦恼。 2. **端到端类型安全(End-to-End Type Safety)**:RPC 接口定义在共享的 `core/commonMain` 中。一旦接口方法或传输对象(DTO)发生改变,前后端在编译期便会直接检查并报错,彻底杜绝接口联调中的低级错误。 3. **重构极度友好**:在 IDE(IDEA / Android Studio / Fleet)中对数据字段或接口函数执行重命名或修改签名,全栈代码会自动联动更新。 4. **原生支持双向流式传输(Streaming)**:借助于 Kotlin 协程与 `Flow`,RPC 可以轻松实现实时消息流、双向数据流推送。 ### JetBrains 官方方案:kotlinx-rpc JetBrains 推出了官方库 **`kotlinx-rpc`**,它是专为 Kotlin Multiplatform 打造的轻量级、跨平台 RPC 框架,天然与 Ktor 和 Kotlinx Serialization 深度集成。 SpringBoot 的 RPC 与 Ktor 不完全一致,推荐使用 gRPC,Apache Dubbo 常用于服务器间的通信。 #### 项目依赖配置 RPC 的通信契约定义在 `core` 模块,服务端与客户端分别引入对应的运行时插件与依赖: **各模块 `build.gradle.kts` 配置示例:** ```kotlin // 1. 在根工程或者对应模块中引入 kotlinx-rpc 插件 plugins { id("org.jetbrains.kotlinx.rpc.plugin") version "0.5.1" // 请参考官方最新版本 kotlin("plugin.serialization") version "2.0.0" } // 2. core/build.gradle.kts (共享接口与数据模型) kotlin { sourceSets { commonMain.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-rpc-core:0.5.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") } } } // 3. server/build.gradle.kts (服务端) dependencies { implementation(project(":core")) implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-server:0.5.1") implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.5.1") implementation("io.ktor:ktor-server-websockets:2.3.12") } // 4. shared/build.gradle.kts (客户端共享层) kotlin { sourceSets { commonMain.dependencies { implementation(project(":core")) implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-client:0.5.1") implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.5.1") implementation("io.ktor:ktor-client-websockets:2.3.12") } } } ``` #### 在 `core/commonMain` 中定义接口与 DTO 在共享层中,我们只需要使用普通接口继承 `RPC`,并用 `@Serializable` 标记传输对象: **最好一个文件只写一个 class** ```kotlin package com.example.core.rpc import kotlinx.rpc.RPC import kotlinx.serialization.Serializable import kotlinx.coroutines.flow.Flow // 共享的数据传输对象 (DTO) @Serializable data class UserDto( val id: Long, val name: String, val role: String ) @Serializable data class NoticeMessage( val id: String, val content: String, val timestamp: Long ) // 共享的 RPC 接口定义 interface UserService : RPC { // 普通 RPC 异步方法调用 suspend fun getUserProfile(userId: Long): UserDto // 接收参数并更新 suspend fun updateUserName(userId: Long, newName: String): Boolean // 使用 Flow 实现服务器向客户端的实时流式推送 fun subscribeNotices(): Flow } ``` #### 在 `server` 模块实现业务并挂载 在服务端,编写接口的具体业务实现,并将其挂载到 Ktor 路由中(通常通过 WebSocket 通信): ```kotlin package com.example.server.services import com.example.core.rpc.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.delay class UserServiceImpl : UserService { override suspend fun getUserProfile(userId: Long): UserDto { // 查数据库或执行业务计算 return UserDto(id = userId, name = "KMP 开发者", role = "Admin") } override suspend fun updateUserName(userId: Long, newName: String): Boolean { println("更新用户 $userId 的姓名为: $newName") return true } override fun subscribeNotices(): Flow = flow { var count = 0 while (true) { emit(NoticeMessage("id_$count", "系统广播消息 #$count", System.currentTimeMillis())) count++ delay(5000) // 每 5 秒推送一条 } } } ``` 在 Ktor 的 `Application.kt` 路由中挂载 RPC 引擎: ```kotlin package com.example.server import io.ktor.server.application.* import io.ktor.server.routing.* import io.ktor.server.websocket.* import kotlinx.rpc.krpc.ktor.server.rpc import kotlinx.rpc.krpc.serialization.json.json import com.example.core.rpc.UserService import com.example.server.services.UserServiceImpl fun Application.configureRpc() { install(WebSockets) // 安装 WebSocket 插件 routing { // 挂载 RPC 端点 rpc("/rpc") { rpcConfig { serialization { json() } } // 注册服务实现类 registerService { UserServiceImpl() } } } } ``` #### 在客户端(`shared/commonMain`)调用服务 在跨平台前端共享代码中,初始化 RPC Client 并获取服务代理: ```kotlin package com.example.shared.network import io.ktor.client.* import io.ktor.client.plugins.websocket.* import kotlinx.rpc.krpc.ktor.client.rpc import kotlinx.rpc.krpc.ktor.client.rpcConfig import kotlinx.rpc.krpc.serialization.json.json import kotlinx.rpc.withService import com.example.core.rpc.UserService import com.example.core.rpc.UserDto class RpcClientManager( private val host: String = "127.0.0.1", private val port: Int = 8080 ) { private val httpClient = HttpClient { install(WebSockets) } suspend fun createUserService(): UserService { val rpcClient = httpClient.rpc { url { this.host = this@RpcClientManager.host this.port = this@RpcClientManager.port encodedPath = "/rpc" } rpcConfig { serialization { json() } } } // 获取由 kotlinx-rpc 动态生成的 UserService 代理实现 return rpcClient.withService() } } ``` 在 UI 层或业务层直接调用: ```kotlin // 先开一个协程 coroutineScope.launch { try { val userService = rpcClientManager.createUserService() // 1. 调用远程挂起函数 val user: UserDto = userService.getUserProfile(1001) println("成功获取用户: ${user.name}") // 2. 收集服务端的实时 Flow 流 userService.subscribeNotices().collect { notice -> println("收到广播: ${notice.content}") } } catch (e: Exception) { println("RPC 通信异常: ${e.message}") } } ``` ### 其他 RPC 方案的对比与选型 根据项目场景的不同,全栈 KMP 还支持以下常见方案: | 方案 | 协议与传输层 | 优势 | 适用场景 | | :--- | :--- | :--- | :--- | | **kotlinx-rpc** | WebSocket / HTTP 等 (kRPC) | Kotlin 官方出品,全平台开箱即用,纯 Kotlin 接口定义,开发效率最高 | 纯 Kotlin / KMP 全栈应用、前后端统一开发 | | **gRPC / Wire** | HTTP/2 + Protobuf | 跨语言支持最强(Java/Go/Python/C++),传输体积小、二进制序列化速度快 | 混合语言微服务架构,前端为 KMP、后端为既有微服务集群 | | **传统 RESTful (Ktor Client)** | HTTP/1.1 或 HTTP/2 + JSON | 标准通用,对外开放友好,与第三方平台对接便利 | 接口需对外公开(Open API)、或者对接既有老系统 | > [!TIP] > 即使在某些必须使用传统 RESTful 架构的项目中,全栈 KMP 依然可以通过在 `core/commonMain` 中统一管理 `@Serializable` 数据类(Request/Response Model),让前后端共用同一套数据协议,从而显著减少联调错误。 ## 接下来可以看 [目录](../README.md) [下一篇:复杂逻辑与 expect / actual](./复杂逻辑与多平台不同逻辑.md)