Files
From_Kotlin_to_Kotlin-Multi…/Learning/全栈KMP.md
T

11 KiB
Raw Blame History

全栈 KMP

全栈指:同时完成后端服务器的部分和前端用户的部分。

因而,在学习本节之前,最好先看 Kotlin Ktor 或 Kotlin Springboot 的其中一个。如果你想使用 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<T>,RPC 可以轻松实现实时消息流、双向数据流推送。

JetBrains 官方方案:kotlinx-rpc

JetBrains 推出了官方库 kotlinx-rpc,它是专为 Kotlin Multiplatform 打造的轻量级、跨平台 RPC 框架,天然与 Ktor 和 Kotlinx Serialization 深度集成。

SpringBoot 的 RPC 与 Ktor 不完全一致,推荐使用 gRPC,Apache Dubbo 常用于服务器间的通信。

项目依赖配置

RPC 的通信契约定义在 core 模块,服务端与客户端分别引入对应的运行时插件与依赖:

各模块 build.gradle.kts 配置示例:

// 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

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<NoticeMessage>
}

在 server 模块实现业务并挂载

在服务端,编写接口的具体业务实现,并将其挂载到 Ktor 路由中(通常通过 WebSocket 通信):

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<NoticeMessage> = flow {
        var count = 0
        while (true) {
            emit(NoticeMessage("id_$count", "系统广播消息 #$count", System.currentTimeMillis()))
            count++
            delay(5000) // 每 5 秒推送一条
        }
    }
}

在 Ktor 的 Application.kt 路由中挂载 RPC 引擎:

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<UserService> { UserServiceImpl() }
        }
    }
}

在客户端(shared/commonMain)调用服务

在跨平台前端共享代码中,初始化 RPC Client 并获取服务代理:

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<UserService>()
    }
}

在 UI 层或业务层直接调用:

// 先开一个协程
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),让前后端共用同一套数据协议,从而显著减少联调错误。

接下来可以看

目录

下一篇:复杂逻辑与 expect / actual