Compare commits

...

8 Commits

Author SHA1 Message Date
SOR2171 0f6d7759a4 docs: 补充全栈 KMP 中的 kotlinx-rpc 实战与方案对比 2026-10-14 16:24:32 +08:00
SOR a25d65dc4d wrong path 2026-10-11 17:45:03 +08:00
SOR2171 4b0f0d80b6 补充 Kotlin 进阶细节与成为超人篇内容 2026-10-10 09:15:42 +08:00
SOR2171 46b2b8f44e 完善 Ktor 与 Spring Boot 基础文档 2026-10-09 23:03:53 +08:00
SOR 8b0aace84a 补充多平台逻辑中的协程小节 2026-10-09 21:15:15 +08:00
SOR 8fe9e2a4e3 完善全栈 KMP 模块架构与共享逻辑设计思路 2026-10-09 21:15:01 +08:00
SOR ace340bed6 补充 Kotlin 进阶中的序列化与 Maven 发布配置 2026-10-09 21:14:46 +08:00
SOR 6ff4414b27 新增 Kotlin Ktor 介绍并更新目录与文档导航 2026-10-09 21:14:30 +08:00
11 changed files with 976 additions and 28 deletions
+192
View File
@@ -0,0 +1,192 @@
# 尝试 Kotlin Ktor
Ktor 是一个比较新的、作为 KMP server 最优选的服务器后端框架。
## 创建一个 Ktor 项目
这很简单,在 IDEA 中就可以创建,仅支持 Kotlin 与 gradle。
引擎选择主要是在 Netty 和 CIO 之间二选一。Netty 最完善,CIO 支持编译为字节码。如果在 JVM 上使用,Netty 功能最强大,CIO 响应更快。具体的代码编写没有差异。
## 依赖添加方法
Ktor 本身的功能很少,所有的复杂功能都由依赖提供。
在 Ktor 里加依赖一般分两步:
### 在 `build.gradle.kts` 中添加依赖
```kotlin
plugins {
kotlin("jvm") version "2.0.0"
id("io.ktor.plugin") version "2.3.12"
// JSON 序列化,版本要和 Kotlin 版本一致
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0"
}
dependencies {
// 引擎:Netty 和 CIO 选一个就行
implementation("io.ktor:ktor-server-netty-jvm")
// implementation("io.ktor:ktor-server-cio-jvm")
// Ktor 基础核心
implementation("io.ktor:ktor-server-core-jvm")
// 常用插件:处理 JSON 请求和返回
implementation("io.ktor:ktor-server-content-negotiation-jvm")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
// 常用插件:处理跨域请求
implementation("io.ktor:ktor-server-cors-jvm")
// 常用插件:全局异常捕获
implementation("io.ktor:ktor-server-status-pages-jvm")
// 日志框架,一般用 logback
implementation("ch.qos.logback:logback-classic:1.5.6")
}
```
### 在代码里使用 `install` 安装
一般我们会给每个插件写一个单独的扩展函数,避免所有代码堆在主入口里:
```kotlin
// 配置 JSON 解析
fun Application.configureSerialization() {
install(ContentNegotiation) {
json() // 这样 Ktor 就能自动把收到的 JSON 转成对应的数据类对象,或者把对象转成 JSON 返回
}
}
// 配置跨域
fun Application.configureHTTP() {
install(CORS) {
anyHost() // 允许任意域名访问,上线时可以改成自己的前端域名
allowHeader(HttpHeaders.ContentType)
}
}
```
## 项目文件的文件夹分类
一个标准的 Ktor 项目,目录结构一般长这样:
- `src/main/kotlin/com/example/`
- `Application.kt`:程序的入口,启动服务器的地方
- `plugins/`:插件配置目录。把各个插件的 `install` 代码分别拆成单独的 `.kt` 文件放这里(比如 `Routing.kt`, `Serialization.kt`, `HTTP.kt`)
- `routes/`:路由目录。类似其他框架里的 Controller,具体的网络接口(GET、POST 路径和参数处理)按业务拆开放这里
- `services/`:业务逻辑目录。处理真正的业务计算、复杂操作,接口调用服务
- `repositories/`:数据持久层。连接数据库、执行 SQL(比如用 Exposed 或原生 JDBC)放这里
- `models/`:数据模型(也可以叫 entity、dto)。**注意:如果你在写全栈 KMP,这里的模型可以直接挪到 `core/commonMain` 里,前端和后端共用一份数据类定义。**
- `src/main/resources/`
- `application.yaml`(或者 `application.conf`):服务器的配置文件
- `logback.xml`:控制台和文件日志输出格式
## 基本的编写思路
写 Ktor 和写传统 Springboot 后端最不一样的地方在于:它没有那么多注解和反射,完全用 Kotlin 本身的特性,因而美观而简洁。
### 入口文件启动服务器
Ktor 通常直接在 `Application.kt` 启动,main 函数不应有任何修改:
```kotlin
package com.example
import io.ktor.server.application.*
import io.ktor.server.netty.*
// 配置文件里指定的入口 module
fun Application.module() {
configureSerialization() // 加载 JSON 插件
configureHTTP() // 加载跨域插件
configureRouting() // 注册网络接口
}
// 在另一个文件的 main 函数
fun main(args: Array<String>): Unit = EngineMain.main(args)
```
### 写路由与接口(Routing)
在 Ktor 里面,不用写复杂的 Controller 类,直接用路由块组织接口:
```kotlin
package com.example.routes
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import com.example.models.User
fun Application.configureRouting() {
routing {
// 访问 GET /
get("/") {
call.respondText("Hello World!")
}
// 可以给一组接口加上统一的前缀
route("/api/users") {
// GET /api/users
get {
call.respond(listOf(User(id = 1, name = "Tom")))
}
// POST /api/users
post {
// 直接把前端传来的 json 反序列化为 User 对象
val user = call.receive<User>()
// 处理你的业务...
call.respond(user)
}
}
}
}
```
因为 Ktor 原生基于协程,所以上面的路由处理函数天然就是挂起函数(`suspend`),可以随意在里面调用挂起函数或者进行异步非阻塞操作,不用担心阻塞主线程。
## 配置文件的写法
Ktor 支持两种配置文件写法:一种是它早期的 `application.conf`(HOCON 格式),另一种是更常见的 `application.yaml`。现在建议直接使用 `application.yaml`,看起来最清楚。
在 `src/main/resources/application.yaml` 里面写:
```yaml
ktor:
deployment:
port: 8080 # 服务器监听的端口
application:
modules:
- com.example.ApplicationKt.module # 指定启动时加载哪个入口函数
# 这里还可以写你自己的配置项
myApp:
jwtSecret: "12345678"
databaseUrl: "jdbc:sqlite:./data.db"
```
在代码里如果想读取这些配置,也很简单,注意,返回值是可空的:
```kotlin
fun Application.module() {
// 读端口
val port = environment.config.property("ktor.deployment.port").getString()
// 读自定义配置
val secret = environment.config.property("myApp.jwtSecret").getString()
}
```
## 接下来可以看
[目录](../README.md)
[下一篇:Kotlin Springboot](./Kotlin-Springboot.md)
[更进一步:成为 Kotlin 超人](./成为Kotlin超人.md)
[进入 KMP](./进入KMP.md)
+196 -1
View File
@@ -2,8 +2,203 @@
> 总体上来讲,Kotlin 与 Java 相比是有好有坏的,优点是写起来更简洁,缺点是写起来更简洁。
Springboot(Spring) 是目前 JVM 平台功能最强大、扩展性最好、健壮性最好的后端框架。
## 创建一个 Springboot 项目
这很简单,在 IDEA 中就可以创建,记得选 Kotlin 和 Gradle kts。
自带插件的话根据你的需要来选就好,如果你只是想利用 Springboot 来做一个命令行小程序也是可以的。
自带插件的话根据你的需要来选就好,如果你只是想利用 Springboot 来做一个命令行小程序也是可以的。
## 插件与依赖
### `build.gradle.kts` 插件配置
新建项目后,你的 `plugins` 里面应该有这几项:
```kotlin
plugins {
// 可以不用定义变量,但要保证版本一致
val kotlinVersion = "2.0.0"
kotlin("jvm") version kotlinVersion
// Spring 专用的 Kotlin 编译器插件
kotlin("plugin.spring") version kotlinVersion
id("org.springframework.boot") version "3.3.0"
id("io.spring.dependency-management") version "1.1.5"
// 如果你用 JPA,建议加上这个,自动给数据实体类生成无参构造函数
// kotlin("plugin.jpa") version kotlinVersion
}
```
### 依赖配置
在 `dependencies` 里,除了常规的 `spring-boot-starter-web`,还需要带上 Kotlin 的反射库:
```kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
// Kotlin 反射库,Spring 容器扫描注入时需要用到
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
```
在 Java 下通常默认用 Jackson 来处理 JSON;而在 Kotlin 下,通常更推荐配合 Kotlin 原生的序列化方案。
## 项目文件的文件夹分类
在 Springboot 里面,基本就是大家非常熟悉的经典 MVC 分层结构。在 Kotlin 里面一般也是这么分:
- `src/main/kotlin/com/example/demo/`
- `DemoApplication.kt`:启动类,里面有 `@SpringBootApplication` 和 `main` 函数
- `controller/`:接收前端请求的 Controller,写路由接口的地方(例如 `UserController.kt`)
- `service/`:业务接口与实现类(例如 `UserService.kt`)
- `repository/` 或 `dao/`:数据库访问层(比如 Spring Data JPA 的接口或 MyBatis 的 Mapper),如果是 Mapper 的话有时会直接写作 `Mapper`。
- `entity/`:数据库表对应的实体类
- `dto/`:数据传输对象(前端传进来的 Request,或者返回给前端的 Response),有时会选择放在 entity 里面
- `config/`:全局配置类(跨域配置、Spring Security 配置等)
- `exception/`:全局异常拦截类(`@RestControllerAdvice`)
- `src/main/resources/`
- `application.yml`:全局配置文件(推荐用 yml,结构比 properties 清晰很多)
## 基本的编写思路
Kotlin 写 Springboot 和 Java 最大的区别,就是 Kotlin 本身的特性减少了大量代码。
### 构造器注入
在 Java 常使用 `@Autowired`,但在 Kotlin 里面,直接用主构造函数用写变量定义即可:
```kotlin
@Service
class UserService(
private val userRepository: UserRepository // 类对象被创建时自动注入对象
) {
fun findUser(id: Long): UserDto {
val user = userRepository.findById(id).orElseThrow()
return UserDto(user.id, user.name)
}
}
```
### 用 `data class` 写 DTO 和数据载体
Java 会使用 Lombok 生成标准 Java Bean 类,Kotlin 的 `data class` 会自动生成 `getter/setter`、`equals`、`hashCode`、`copy`:
```kotlin
data class CreateUserRequest(
val username: String,
val email: String? = null // 问号表示可以为 null,对应选填字段
)
data class UserDto(
val id: Long,
val username: String
)
```
### Controller
Controller 写法和常规 Springboot 一模一样,但由于 Kotlin 支持单行表达式,写起来格外干净:
```kotlin
@RestController
@RequestMapping("/api/users")
class UserController(
private val userService: UserService
) {
@GetMapping("/{id}")
fun getUser(
@PathVariable id: Long
): UserDto {
return userService.findUser(id)
}
@PostMapping
fun createUser(
@RequestBody request: CreateUserRequest
): UserDto {
return userService.createUser(request)
}
}
```
### 全局异常处理
用 `@RestControllerAdvice` 兜底,不用在 Controller 里写 `try-catch`:
```kotlin
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(NoSuchElementException::class)
fun handleNotFound(e: NoSuchElementException): ResponseEntity<String> {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.message)
}
}
```
## 配置文件的写法
Springboot 支持 `.properties` 和 `.yml`,强烈推荐使用 `application.yml`。
在 `src/main/resources/application.yml` 里一般写:
```yaml
server:
port: 8080 # 端口
spring:
application:
name: my-kotlin-app
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: rootpassword
# 自定义配置
my-config:
token-secret: "abcdefg123456"
```
### 在 Kotlin 代码中读取配置
有两种常见方式:
#### @Value
适合只读取一两个简单变量:
```kotlin
@Component
class MyComponent(
@Value("$${my-config.token-secret}") private val tokenSecret: String
)
```
#### @ConfigurationProperties 与 data class
把整块配置映射成一个不可变的数据类,方便快捷:
```kotlin
@ConfigurationProperties(prefix = "my-config")
data class MyConfigProperties(
val tokenSecret: String
)
```
只要在启动类上加上 `@ConfigurationPropertiesScan`,就可以直接把它像普通 Service 一样注入到任何地方使用了。
## 接下来可以看
[目录](../README.md)
[下一篇:Kotlin Ktor](./Kotlin-Ktor.md)
[更进一步:成为 Kotlin 超人](./成为Kotlin超人.md)
[进入 KMP](./进入KMP.md)
+277 -12
View File
@@ -2,25 +2,290 @@
**全栈**指:同时完成后端服务器的部分和前端用户的部分。
因而,在学习本节之前,最好先看 [Kotlin Ktor](./Kotlin-Ktor.md) 或 [Kotlin Springboot](./Kotlin-Springboot.md) 的其中一个。如果你想使用 KMP 最全面的功能,最好是选择 Ktor。
## 一个完整的 KMP 项目
目前(20261009),完整的 KMP 项目包含以下几项:
- Android
- JVM
- iOS
- WASM
- Server
目前(2026.10.09),完整的 KMP 项目包含以下几项:
- app
- androidApp
- jvmApp
- iosApp
- webApp
- shared
- commonMain
- core
- commonMain
- server
除了 iOS 部分必须由 Xcode 做额外设置后编译以外,其他的应用均可在 JDK 下完成编译。
除了 iOS 部分必须由 Xcode 做额外设置后编译以外,其他的应用均可在 JDK 下完成编译(Android 需要相应 SDK,其他需要编译为字节码的需要其他编译器)。
Server 本身也是运行在 JVM 上面的,它和普通的 Java / Kotlin 后端项目基本没有区别。不过,在 KMP 中,将它的部分代码写在 commonMain 中,可以在一定程度上减少代码的出错可能,还可以进行代码复用。
Server 本身也是运行在 JVM 上面的,它和普通的 Java / Kotlin 后端项目基本没有区别,除非使用特殊的 Ktor 后端编译为字节码。不过,在 KMP 中,将它的部分代码写在 `core/commonMain` 中,可以在一定程度上减少代码的出错可能,还可以进行代码复用。
我们在编写全栈 KMP 时,应当优先考虑 WASM 和 Server 能否完美运行。
我们在编写全栈 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` 配置示例:**
```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<NoticeMessage>
}
```
#### 在 `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<NoticeMessage> = 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<UserService> { 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<UserService>()
}
}
```
在 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)
[下一篇:复杂逻辑与多平台不同逻辑](./复杂逻辑与多平台不同逻辑.md)
[下一篇:编写服务器应用 —— SpringBoot](./Kotlin-Springboot.md)
[下一篇:复杂逻辑与 expect / actual](./复杂逻辑与多平台不同逻辑.md)
@@ -2,6 +2,10 @@
> 确保你有至少看过一遍 [深入 Kotlin](./深入Kotlin.md)
## suspend 与协程
当我们需要做一些复杂的事情的时候,例如:
## expect / actual
这东西其实叫`预期与实际机制`,但是真的不好描述,用法也比较多样。
@@ -53,7 +57,7 @@ actual val isDebugMode: Boolean = BuildConfig.DEBUG
actual val isDebugMode: Boolean = Platform.isDebugBinary
```
### 在实验中 / 不稳定的写法(截至20261009)
### 在实验中 / 不稳定的写法(截至2026.10.09)
各种类定义也是可以用 expect / actual 的。这里用普通类举例子:
+2 -2
View File
@@ -123,7 +123,7 @@ class Demo {
### 可空类型
Kotlin 把“可能为空”写进类型里:
Kotlin 有一个专门的可空类型:
```kotlin
var name: String = "Kotlin"
@@ -133,7 +133,7 @@ var nullableName: String? = null
nullableName = "Kotlin"
```
`String?` 表示这个变量可能是 `String`,也可能是 `null`。要访问可空对象,需要安全调用:
`String?` 表示这个变量可能是 `String`,也可能是 `null`。要访问可空对象的属性,需要安全调用:
```kotlin
val length = nullableName?.length ?: 0
+1 -1
View File
@@ -61,7 +61,7 @@ println("Version: ${version + 1}")
#### 可空类型
Kotlin 把“可能为空”写进类型里:
Kotlin 有一套方法处理空指针问题:
```kotlin
var name: String = "Kotlin"
+246 -4
View File
@@ -11,10 +11,252 @@
## JSON 序列化
## Kotlin 协程
Kotlin-serialization 是最佳选择,它需要加载一个 gradle 插件,注意要与你的 Kotlin 版本一致:
## 不依赖 Springboot web 的网络 api
```kotlin
plugins {
kotlin("jvm") version "2.3.20"
kotlin("plugin.serialization") version "2.3.20"
}
```
## 加载动态链接库(JNA)
还有一个依赖
## 将 jar 文件上传至 Maven 仓库
```kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect")
```
## Kotlin 协程与多线程细节
核心依赖:
```kotlin
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
```
要点概括:
- **不要用 `GlobalScope`**:建议自定义 `CoroutineScope(SupervisorJob() + Dispatchers.Default)` 或跟随生命周期,防止协程泄露。
- **线程调度(Dispatchers)**:
- `Dispatchers.Main`:UI 线程(Android/Compose)。
- `Dispatchers.IO`:网络、磁盘文件等阻塞 I/O 操作。
- `Dispatchers.Default`:计算密集型、复杂数据处理。
- 线程切换使用 `withContext(Dispatchers.IO) { ... }` 即可。
- **并发任务**:多个耗时任务同时跑用 `async { ... }`,最后再调用 `.await()` 获取结果。
- **响应式数据流**:状态监听推荐用 `StateFlow` 或 `SharedFlow`,替代传统的回调接口。
## 不依赖网络框架的网络 api
如果你不想引入 Spring Boot 或 Ktor 这样庞大的框架,只是想发个请求或者写个简单的接口:
### 发送 HTTP 请求(客户端)
直接用 Java 11+ 原生自带的 `HttpClient` 即可,零依赖且原生支持异步:
```kotlin
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
val client = HttpClient.newHttpClient()
val request = HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com"))
.GET()
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
println(response.body())
```
如果是 KMP 跨平台客户端,首选官方轻量客户端 `io.ktor:ktor-client-core`。
### 极简 HTTP 服务端
JDK 自带了轻量级的 `HttpServer`,适合写小工具或内网 Mock:
```kotlin
import com.sun.net.httpserver.HttpServer
import java.net.InetSocketAddress
val server = HttpServer.create(InetSocketAddress(8080), 0)
server.createContext("/ping") { exchange ->
val response = "pong"
exchange.sendResponseHeaders(200, response.length.toLong())
exchange.responseBody.use { it.write(response.toByteArray()) }
}
server.start()
```
## 加载动态链接库(JNA)
如果需要调用 C/C++ 编译出的动态链接库(`.dll` / `.so` / `.dylib`),比起折腾麻烦的 JNI 头文件和胶水代码,**JNA (Java Native Access)** 是 JVM 和 Android 上最轻松的选择。
依赖:
```kotlin
dependencies {
implementation("net.java.dev.jna:jna:5.14.0")
// 如果是 Android 端则使用:
// implementation("net.java.dev.jna:jna:5.14.0@aar")
}
```
使用时只需要定义一个接口继承 `com.sun.jna.Library`:
```kotlin
import com.sun.jna.Library
import com.sun.jna.Native
interface MyNativeLib : Library {
fun add(a: Int, b: Int): Int
companion object {
// "mylib" 对应 libmylib.so / mylib.dll
val INSTANCE: MyNativeLib = Native.load("mylib", MyNativeLib::class.java)
}
}
// 直接调用 C 函数
val result = MyNativeLib.INSTANCE.add(1, 2)
```
注意,库文件需要放在特定的文件夹,这里参考[官方文档](https://github.com/java-native-access/jna/blob/master/www/GettingStarted.md)
## 与 C 混编(cinterop)
在 Kotlin/Native(编译为原生二进制的目标平台,如 iOS、Linux、macOS、Windows)中,不需要 JVM 和 JNA,Kotlin 官方直接提供了 **cinterop** 工具。
1. 在项目源码旁新建 `.def` 文件(例如 `src/nativeInterop/cinterop/myclib.def`):
```properties
headers = myclib.h
staticLibraries = libmyclib.a
libraryPaths = /path/to/lib
```
2. 在 `build.gradle.kts` 中声明 cinterop:
```kotlin
kotlin {
linuxX64 {
binaries.executable()
compilations.getByName("main").cinterops {
val myclib by creating
}
}
}
```
3. Gradle 构建后会自动生成 Kotlin 绑定包,在代码中直接 import 即可像调用普通 Kotlin 函数一样调用 C 函数,并通过 `memScoped` 安全管理 C 指针与内存。
## 委托(Delegation 与 by 关键字)
Kotlin 的 `by` 能极大精简代码,主要用在两个地方:
### 类委托
不必手动重写接口的所有方法再转发给内部对象,直接 `by` 代理:
```kotlin
interface Repository {
fun fetch(): String
fun save(data: String)
}
// 缓存包装类,未重写的方法自动转发给 inner
class CachedRepository(private val inner: Repository) : Repository by inner {
override fun fetch(): String {
return "from cache" // 只需重写你要拦截的方法
}
}
```
### 属性委托
除了常见的 `by lazy`,只要类提供了 `operator fun getValue / setValue`,就能代理属性的读写(Compose / KMP 里的 `var text by remember { mutableStateOf("") }` 就是如此):
```kotlin
// 监听属性变化的标准库委托
var name: String by Delegates.observable("初始值") { prop, old, new ->
println("$old -> $new")
}
```
## 内联值类
如果想给基础类型(如 `Long`、`Int`)包一层业务类型(例如 `UserId`、`OrderId`)以保证强类型安全,又不想每次创建对象都在堆内存里产生 GC 开销:
```kotlin
@JvmInline
value class UserId(val value: Long)
fun getUser(id: UserId) { ... }
```
编译后会在字节码里直接拆包还原成底层的原始类型,完全是零运行时开销。Compose 里的 `Color`、`Dp` 底层全部是用它实现的。
## 优雅的异常处理:runCatching
在现代 Kotlin 代码中,尽量少写层层嵌套的 `try-catch`。标准库的 `runCatching` 更加函数式和连贯:
```kotlin
val result = runCatching {
// 可能会抛异常的代码
"123".toInt()
}.getOrDefault(0) // 报错直接返回默认值,也可使用 getOrElse { ... }
// 配合链式调用
runCatching { fetchNetworkData() }
.onSuccess { data -> println("成功: $data") }
.onFailure { error -> println("失败: ${error.message}") }
```
## 大集合流式处理:Sequence
普通集合操作(`list.filter { }.map { }`)每一步都会立即创建一个全新的临时 `List`,在处理大数据或长链条时浪费大量内存。
加上 `asSequence()` 开启惰性求值(类似 Java 的 Stream),一个元素走完所有变换才处理下一个:
```kotlin
val result = (1..100000).asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.take(10) // 只计算出前 10 个就提前停止,后面的根本不会去算
.toList()
```
## Java 互操作注解
当 Kotlin 代码需要被 Java 代码调用,或者被依赖 Java 反射的旧框架识别时,别忘了这几个注解:
- **`@JvmOverloads`**:给带有默认参数的函数自动生成 Java 重载方法,否则 Java 只能看到全参方法。
- **`@JvmStatic`**:把 `companion object` 或 `object` 里的函数真正编译成 Java 视角的 `static` 方法。
- **`@JvmField`**:直接暴露为底层公有字段,不生成 Kotlin 默认的 getter/setter。
## 注解的使用处目标(Use-site Targets)
Kotlin 声明一个属性 `val name: String`,在字节码里其实包含了:底层字段(field)、getter 方法、构造函数参数。
当配合 Spring、Room、JSON 序列化等注解使用时,如果不显式指定,注解可能会打在 getter 上导致框架失效。此时必须加上前缀:
```kotlin
data class User(
@field:NotBlank // 作用于字段
val username: String,
@get:JsonIgnore // 作用于 getter 方法上
val secretToken: String
)
```
## 将 jar 包上传至 Maven 仓库
这需要一个 gradle 插件:
```kotlin
plugins {
id("com.vanniktech.maven.publish") version "0.37.0"
}
```
具体的写法可以参考 [FFmpeg-Kit_KMP](https://github.com/SOR2171/FFmpeg-Kit_KMP/blob/main/settings.gradle.kts) 的 gradle 配置。
+47 -1
View File
@@ -886,7 +886,7 @@ println("是否为可变长参数: ${param.isVararg}")
## 定义一个特殊的变量/函数名
只需要使用反引号 ``` ` ``` 将一个任意符串包裹起来,它几乎可以被定义为所有东西的名称。
只需要使用反引号 ``` ` ``` 将一个任意字符串包裹起来,它几乎可以被定义为任何东西的名称。
```kotlin
fun `fun`() = "fun" // 例如定义一个函数
@@ -894,6 +894,50 @@ val `this` = 1 // 或是一个变量
val `or write a sentence` = true
```
## 类型别名 typealias
当某些类型名字特别长,或者某些 lambda 函数类型签名较长时,写起来会很啰嗦。使用 `typealias` 可以给现有类型起一个简单易读的别名。
```kotlin
// 给复杂的函数类型起别名
typealias ClickListener = (viewId: Int, message: String) -> Unit
fun setOnClick(listener: ClickListener) {
listener(1, "clicked")
}
// 给长泛型起别名
typealias UserMap = Map<String, List<Set<Int>>>
// 在 KMP 中非常关键:直接将平台已有类型映射给 commonMain 的公共声明
expect class AtomicInt
actual typealias AtomicInt = java.util.concurrent.atomic.AtomicInteger
```
## Nothing 类型与 TODO()
在 Kotlin 中,`Nothing` 是一个非常特殊的底层类型:它是**所有其他类型的子类型(Bottom type)**。
它表示“这个函数永远不会正常返回”,比如抛出异常或者死循环的函数:
```kotlin
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
```
因为 `Nothing` 是所有类型的子类,所以在类型推断和空安全配合时非常方便:
```kotlin
// 1. Elvis 操作符配合抛异常,编译器会自动把 Noting 识别为非空 String
val name: String = inputName ?: fail("Name is required")
// 2. TODO() 占位
fun calculateComplexLogic(): Int {
TODO("还没写完,占个位编译不报错") // TODO() 的返回值就是 Nothing
}
```
## IDEA 的常用快捷键
`ctrl + alt + L`:格式化代码
@@ -910,6 +954,8 @@ val `or write a sentence` = true
[下一篇:Kotlin Springboot](./Kotlin-Springboot.md)
[下一篇:Kotlin Ktor](./Kotlin-Ktor.md)
[更进一步:成为 Kotlin 超人](./成为Kotlin超人.md)
[进入 KMP](./进入KMP.md)
+2 -2
View File
@@ -21,7 +21,7 @@
## 跑起来看看效果
Gradle 默认会注册启动项 `hotRun`,它会被 IDEA / Android Studio 识别为 `desktopApp [hot] 🔥`。这是开发阶段的主要的测试方法。点击,即可启动编译,并运行热重载程序,它可以在你修改源代码并编译后,直接在原有程序窗口同步你的修改。
Gradle 默认会注册启动项 `hotRun`,如果是在 IDEA / Android Studio 创建的项目,会在 IDE 中添加一个叫 `desktopApp [hot] 🔥` 的配置文件。这是开发阶段的主要的测试方法。点击,即可启动编译,并运行热重载程序,它可以在你修改源代码并编译后,直接在原有程序窗口同步你的修改。
## commonMain 编写设计
@@ -77,7 +77,7 @@ Column 内部,有一个 `Button`,还有一个被 `AnimatedVisibility` 框起
我们来看它的 showContent 变量如何被定义,然后在 Button 被点击时被改变
```kotlin
// 将 showContent 委托给一个 Compose state
// 将 showContent 委托给一个 Compose State
var showContent by remember { mutableStateOf(false) }
// 一些排版
+2 -2
View File
@@ -25,13 +25,13 @@
- build.gradle.kts
- .gitignore
- README.md
- gradlew (二进制脚本,一个 bat,一个 sh)
- gradlew (脚本文件,一个 bat,一个 sh)
- gradle.proporties
- local.proporties (默认被 ignore)
- build.gradle.kts
- settings.gradle.kts
如果你的项目还要包含 server 的部分的话,那会稍微复杂一点点。
如果你的项目还要包含 server 的部分的话,那会稍微复杂一点点,这个后面再说。
### 各部分用途
+6 -2
View File
@@ -2,6 +2,9 @@
本教程支持使用 AI 概括 / 解释。
考虑到 KMP 仍然处在不断测试,不断发展的阶段,希望以后的 APP 部长可以继续更新这个教程。
## 目录
### Kotlin
@@ -9,11 +12,12 @@
1. [引入 Kotlin](Learning/引入Kotlin.md)
2. [深入 Kotlin](Learning/深入Kotlin.md)
3. [尝试 Kotlin Springboot](Learning/Kotlin-Springboot.md)
4. [成为 Kotlin 超人](Learning/成为Kotlin超人.md)
4. [尝试 Kotlin Ktor](Learning/Kotlin-Ktor.md)
5. [成为 Kotlin 超人](Learning/成为Kotlin超人.md)
### Kotlin Multiplatform
1. [进入 KMP](learning/进入KMP.md)
1. [进入 KMP](Learning/进入KMP.md)
2. [第一个 KMP 应用](Learning/第一个KMP应用.md)
3. [复杂逻辑与 expect / actual](Learning/复杂逻辑与多平台不同逻辑.md)
4. [全栈 KMP](Learning/全栈KMP.md)