一、 引言:KMP技术栈的演进与全栈愿景
Kotlin Multiplatform (KMP) 作为 JetBrains 推出的跨平台解决方案,正从移动端(Android/iOS)向更广阔的全栈领域迈进。本文将探讨如何利用 KMP 构建从移动端到服务端,乃至新兴 AI Agent 应用的全栈技术体系。
二、 KMP 核心能力与生态现状
- 共享业务逻辑: 在 Android、iOS、Web、桌面端复用核心代码。
- 原生 UI 体验: 各平台使用原生 UI 框架,保持最佳用户体验。
- 日渐成熟的生态: Compose Multiplatform、Ktor、SQLDelight 等框架的支持。
- 与现有技术的融合: 如何与 Spring Boot、Node.js 等后端技术栈协同。
三、 第一站:巩固移动端根基 (Android/iOS)
- 架构设计: 使用 KMP 构建跨平台的 ViewModel、Repository、数据模型。
- 网络与数据层共享: 使用 Ktor Client 和 Kotlinx.serialization 实现统一的网络请求与序列化。
- 状态管理: 在共享模块中管理应用状态,通过 expect/actual 对接平台特性。
- 实战案例: 一个简单的跨平台待办事项应用 (Todo App) 的 KMP 实现。
实战案例:跨平台待办事项应用 (Todo App) 核心代码示例
以下是一个 Todo App 的 KMP 共享模块核心代码骨架,展示了数据模型、Repository 和 ViewModel 的 expect/actual 定义。
1. 共享数据模型 (commonMain)
kotlin
// commonMain/kotlin/com/example/todo/shared/model/TodoItem.kt
package com.example.todo.shared.model
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/**
待办事项数据模型,在共享模块中定义,Android/iOS 通用。
*/
@Serializable
data class TodoItem(
val id: String,
val title: String,
val description: String? = null,
val isCompleted: Boolean = false,
val createdAt: Instant = Instant.now(),
val updatedAt: Instant = Instant.now()
)
2. 共享 Repository 接口 (commonMain)
kotlin
// commonMain/kotlin/com/example/todo/shared/repository/TodoRepository.kt
package com.example.todo.shared.repository
import com.example.todo.shared.model.TodoItem
import kotlinx.coroutines.flow.Flow
/**
待办事项仓库接口(expect)。
各平台需提供 actual 实现,处理平台特定的数据持久化(如 SQLite、UserDefaults 等)。
*/
expect class TodoRepository {
suspend fun getAllTodos(): Flow<List<TodoItem>>
suspend fun getTodoById(id: String): TodoItem?
suspend fun insertTodo(todo: TodoItem)
suspend fun updateTodo(todo: TodoItem)
suspend fun deleteTodo(id: String)
suspend fun toggleTodoCompletion(id: String)
}
3. Android 平台 Repository 实现 (androidMain)
kotlin
// androidMain/kotlin/com/example/todo/shared/repository/TodoRepository.kt
package com.example.todo.shared.repository
import android.content.Context
import com.example.todo.shared.model.TodoItem
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.datetime.Instant
/**
Android 平台的 TodoRepository 实际实现。
此处使用内存存储简化示例,实际项目可替换为 Room、SQLDelight 等。
*/
actual class TodoRepository actual constructor() {
private val _todos = MutableStateFlow<List<TodoItem>>(emptyList())
actual override suspend fun getAllTodos(): Flow<List<TodoItem>> = _todos.asStateFlow()
actual override suspend fun getTodoById(id: String): TodoItem? {
return _todos.value.find { it.id == id }
}
actual override suspend fun insertTodo(todo: TodoItem) {
_todos.value = _todos.value + todo
}
actual override suspend fun updateTodo(todo: TodoItem) {
_todos.value = _todos.value.map { if (it.id == todo.id) todo else it }
}
actual override suspend fun deleteTodo(id: String) {
_todos.value = _todos.value.filterNot { it.id == id }
}
actual override suspend fun toggleTodoCompletion(id: String) {
_todos.value = _todos.value.map { todo ->
if (todo.id == id) todo.copy(isCompleted = !todo.isCompleted, updatedAt = Instant.now())
else todo
}
}
}
4. iOS 平台 Repository 实现 (iosMain)
kotlin
// iosMain/kotlin/com/example/todo/shared/repository/TodoRepository.kt
package com.example.todo.shared.repository
import com.example.todo.shared.model.TodoItem
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.datetime.Instant
/**
iOS 平台的 TodoRepository 实际实现。
同样使用内存存储简化,实际可使用 SQLDelight(通过 CocoaPods)或 UserDefaults。
*/
actual class TodoRepository actual constructor() {
private val _todos = MutableStateFlow<List<TodoItem>>(emptyList())
actual override suspend fun getAllTodos(): Flow<List<TodoItem>> = _todos.asStateFlow()
actual override suspend fun getTodoById(id: String): TodoItem? {
return _todos.value.find { it.id == id }
}
actual override suspend fun insertTodo(todo: TodoItem) {
_todos.value = _todos.value + todo
}
actual override suspend fun updateTodo(todo: TodoItem) {
_todos.value = _todos.value.map { if (it.id == todo.id) todo else it }
}
actual override suspend fun deleteTodo(id: String) {
_todos.value = _todos.value.filterNot { it.id == id }
}
actual override suspend fun toggleTodoCompletion(id: String) {
_todos.value = _todos.value.map { todo ->
if (todo.id == id) todo.copy(isCompleted = !todo.isCompleted, updatedAt = Instant.now())
else todo
}
}
}
5. 共享 ViewModel (commonMain)
kotlin
// commonMain/kotlin/com/example/todo/shared/viewmodel/TodoViewModel.kt
package com.example.todo.shared.viewmodel
import com.example.todo.shared.model.TodoItem
import com.example.todo.shared.repository.TodoRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/**
待办事项的共享 ViewModel。
业务逻辑(状态管理、事件处理)在共享模块中编写,与平台 UI 框架解耦。
*/
class TodoViewModel(private val repository: TodoRepository) {
private val _uiState = MutableStateFlow(TodoUiState())
val uiState: StateFlow<TodoUiState> = _uiState.asStateFlow()
private val coroutineScope = CoroutineScope(Dispatchers.Default)
init {
loadTodos()
}
private fun loadTodos() {
coroutineScope.launch {
repository.getAllTodos().collect { todos ->
_uiState.update { it.copy(todos = todos) }
}
}
}
fun addTodo(title: String, description: String? = null) {
coroutineScope.launch {
val newTodo = TodoItem(
id = System.currentTimeMillis().toString(),
title = title,
description = description
)
repository.insertTodo(newTodo)
}
}
fun toggleTodoCompletion(id: String) {
coroutineScope.launch {
repository.toggleTodoCompletion(id)
}
}
fun deleteTodo(id: String) {
coroutineScope.launch {
repository.deleteTodo(id)
}
}
}
/**
UI 状态数据类,同样定义在共享模块中。
*/
data class TodoUiState(
val todos: List<TodoItem> = emptyList(),
val isLoading: Boolean = false,
val errorMessage: String? = null
)
架构说明:
- 数据模型 (TodoItem) :使用
kotlinx.serialization序列化,确保在 Android/iOS 间传输时类型安全。 - Repository 层 :通过
expect/actual机制,共享接口,隔离平台具体实现(如 Android 用 Room,iOS 用 SQLDelight 或 Core Data)。 - ViewModel 层 :纯 Kotlin 协程实现,管理状态和业务逻辑,可在 Android 的
ViewModel和 iOS 的@State对象中复用。 - 平台 UI :Android 端使用 Jetpack Compose 或 View 系统,iOS 端使用 SwiftUI 或 UIKit,分别观察
TodoViewModel.uiState并调用其方法。
四、 进军服务端:KMP 作为后端技术栈
- 为什么选择 KMP 做后端?: 代码复用、团队技能统一、性能考量。
- 技术选型: Ktor Server、Exposed ORM、Koin/Dagger 依赖注入。
下表对比了 KMP 后端开发中几个核心技术的选型考量:
| 技术选项 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Ktor Server | * 轻量级、高性能,专为 Kotlin 协程设计 * 与 KMP 生态无缝集成,代码复用度高 * DSL 风格配置,开发体验流畅 * 模块化设计,可按需引入功能 | * 生态相对 Spring Boot 等仍较小 * 企业级功能(如监控、链路追踪)需自行集成或选择第三方 * 学习曲线对传统 Java EE 开发者可能较陡 | * 中小型项目、微服务、API Gateway * 需要快速原型或对性能有较高要求的服务 * 团队已熟悉 Kotlin 协程和函数式编程 |
| Exposed ORM | * 纯 Kotlin DSL,类型安全,编译期检查 * 轻量,无反射,与 KMP 共享模块兼容性好 * 支持 SQL 和 DAO 两种模式,灵活度高 * JetBrains 官方维护,与 IntelliJ IDEA 集成好 | * 高级 ORM 特性(如懒加载、复杂关联映射)较弱 * 社区插件和工具链不如 Hibernate 丰富 * 对复杂查询的 DSL 学习有一定成本 | * 中小型项目,数据结构相对简单 * 追求轻量、无反射、类型安全的数据库访问层 * 需要在 KMP 共享模块中复用数据模型定义 |
| Koin | * 纯 Kotlin,轻量级,无代码生成,无反射 * DSL 简洁直观,学习成本低 * 与 KMP 和 Kotlin 协程天然契合 * 启动快,运行时开销小 | * 功能相对 Dagger 较简单,缺少编译时严格检查 * 复杂依赖图或动态依赖场景下可能不够灵活 * 生态和社区规模小于 Dagger | * 中小型 KMP 项目,追求开发效率和简洁性 * 团队偏好纯 Kotlin 解决方案,避免注解处理器 * 依赖关系相对简单,不需要复杂的生命周期管理 |
| Dagger/Hilt | * 编译时依赖注入,性能好,错误提前暴露 * 功能强大,支持复杂的依赖图和生命周期 * 生态成熟,社区资源丰富,最佳实践多 * Android 官方推荐,与 Jetpack 集成度高 | * 配置复杂,学习曲线陡峭 * 依赖注解处理器,可能增加构建时间 * 在纯 Kotlin/非 Android 环境中配置稍显繁琐 | * 大型、长期维护的项目,需要严格的架构约束 * Android 平台为主或需要与现有 Android 项目深度集成 * 团队已熟悉 Dagger/Hilt,且需要其强大功能 |
- 构建 RESTful API: 设计、实现并部署一个为移动端提供服务的 KMP 后端。
- 数据库操作: 使用 SQLDelight 或 Exposed 进行跨平台的数据持久化设计。
- 身份认证与授权: JWT 等认证机制在 KMP 全栈中的实现。
五、 全栈贯通:前后端一体化开发实践
- 共享数据模型与 API 契约: 使用 Kotlin 定义 DTO,并通过共享模块确保前后端类型安全。
- 端到端类型安全: 告别手写 API 文档和客户端代码,实现编译期检查。
- 开发效率提升: 修改共享业务逻辑,自动同步到所有平台。
- 调试与联调: 在全栈 KMP 项目中的调试技巧与工具链。
六、 迈向未来:KMP 在 AI Agent 开发中的角色
- AI Agent 是什么?: 简述自主性、工具使用、长期记忆等核心概念。
- KMP 的优势: 为何适合作为 AI Agent 的"大脑"或"协调层"?
- 架构设想: 使用 KMP 共享模块封装 Agent 核心逻辑(工作流、记忆、工具调用)。
- 平台适配 :
- 移动端 Agent: 作为本地智能助手,处理设备传感器数据。
- 服务端 Agent: 作为后端服务,处理复杂、耗时的自动化任务。
- 桌面端 Agent: 提供强大的本地计算和交互界面。
- 技术集成: 如何将 LangChain/Kotlin、OpenAI API 等 AI 能力封装进 KMP 共享模块。
技术集成:在 KMP 共享模块中封装 AI 能力
将 AI 能力(如 LangChain/Kotlin 或 OpenAI API)集成到 KMP 共享模块的核心思想是:在共享代码中定义统一的 AI 服务接口(expect),然后在各平台(Android、iOS、JVM)提供具体的实现(actual)。这样,业务逻辑(如 Agent 的工作流、工具调用)就可以在共享模块中编写,而平台特定的网络、安全或 UI 交互细节则被隔离。
1. 定义共享的 AI 服务接口
首先,在 KMP 共享模块的 commonMain 源集中,定义一个 expect 类或接口,它抽象了 AI 服务的基本操作,例如聊天补全、工具调用等。
kotlin
// commonMain/kotlin/com/example/ai/AIService.kt
import kotlinx.coroutines.flow.Flow
/**
在 KMP 共享模块中期望的 AI 服务接口。
各平台需提供 actual 实现。
*/
expect class AIService(apiKey: String) {
/**
发送聊天消息并获取流式响应。
@param messages 聊天消息历史
@param model 使用的模型标识符(如 "gpt-4o-mini")
@param temperature 温度参数
@return 响应内容的 Flow(用于流式输出)
*/
suspend fun chatCompletions(
messages: List<ChatMessage>,
model: String = "gpt-4o-mini",
temperature: Double = 0.7
): Flow<String>
/**
发送聊天消息并获取一次性完整响应。
*/
suspend fun chatCompletionsBlocking(
messages: List<ChatMessage>,
model: String = "gpt-4o-mini",
temperature: Double = 0.7
): String
// 可扩展更多方法,如工具调用、嵌入生成等
}
/**
聊天消息数据类(在共享模块中定义,前后端通用)。
*/
data class ChatMessage(
val role: String, // "system", "user", "assistant"
val content: String
)
2. 在各平台提供 actual 实现
接下来,在对应平台的源集(如 androidMain、iosMain、jvmMain)中,使用平台特定的 HTTP 客户端或 SDK 来实现上述接口。
Android/JVM 实现(使用 Ktor Client)
对于 Android 和 JVM(后端)平台,我们可以使用 Ktor Client 来调用 OpenAI 的 HTTP API。
kotlin
// androidMain/kotlin/com/example/ai/AIService.kt
// jvmMain/kotlin/com/example/ai/AIService.kt (内容相同)
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.flow.flow
actual class AIService actual constructor(private val apiKey: String) {
// 使用 Ktor Client,需在对应平台的依赖中引入
private val client = HttpClient {
expectSuccess = false
defaultRequest {
url("https://api.openai.com/v1/")
header(HttpHeaders.Authorization, "Bearer $apiKey")
header(HttpHeaders.ContentType, ContentType.Application.Json)
}
}
actual override suspend fun chatCompletions(
messages: List<ChatMessage>,
model: String,
temperature: Double
): Flow<String> = flow {
val response: HttpResponse = client.post("chat/completions") {
setBody(
mapOf(
"model" to model,
"messages" to messages.map { it.toMap() },
"temperature" to temperature,
"stream" to true // 启用流式响应
)
)
}
// 简化处理:逐行读取 SSE 流并发射内容
response.bodyAsChannel().toByteArray().decodeToString().lines().forEach { line ->
if (line.startsWith("data: ")) {
val data = line.removePrefix("data: ")
if (data != "[DONE]") {
// 解析 JSON 提取 content(此处简化)
emit(data)
}
}
}
}
actual override suspend fun chatCompletionsBlocking(
messages: List<ChatMessage>,
model: String,
temperature: Double
): String {
val response: HttpResponse = client.post("chat/completions") {
setBody(
mapOf(
"model" to model,
"messages" to messages.map { it.toMap() },
"temperature" to temperature,
"stream" to false
)
)
}
val json = response.body<Map<String, Any>>()
// 简化解析,实际应使用 kotlinx.serialization
return (json["choices"] as List<Map<String, Any>>).first()["message"] as Map<String, String>).getValue("content")
}
private fun ChatMessage.toMap() = mapOf("role" to role, "content" to content)
}
iOS 实现(使用 NSURLSession)
在 iOS 平台,我们可以使用 Foundation 的 NSURLSession 来实现网络请求,包括流式响应。
kotlin
// iosMain/kotlin/com/example/ai/AIService.kt
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow
import platform.Foundation.*
actual class AIService actual constructor(private val apiKey: String) {
actual override suspend fun chatCompletions(
messages: List<ChatMessage>,
model: String,
temperature: Double
): Flow<String> = callbackFlow {
val url = NSURL.URLWithString("https://api.openai.com/v1/chat/completions")!!
val request = NSMutableURLRequest.requestWithURL(url)
request.setHTTPMethod("POST")
request.setValue("Bearer $apiKey", forHTTPHeaderField = "Authorization")
request.setValue("application/json", forHTTPHeaderField = "Content-Type")
request.setValue("text/event-stream", forHTTPHeaderField = "Accept")
// 构建请求体
val requestBody = NSMutableDictionary()
requestBody.setObject(model, forKey = "model")
requestBody.setObject(messages.map { it.toMap() }, forKey = "messages")
requestBody.setObject(temperature, forKey = "temperature")
requestBody.setObject(true, forKey = "stream")
val jsonData = try {
NSJSONSerialization.dataWithJSONObject(requestBody, 0, null)
} catch (e: NSException) {
close(RuntimeException("Failed to serialize request body: ${e.description}"))
return@callbackFlow
}
request.HTTPBody = jsonData
// 创建 NSURLSessionDataTask 处理 SSE 流
val task = NSURLSession.sharedSession.dataTaskWithRequest(request) { data, response, error ->
try {
if (error != null) {
val nsError = error as NSError
close(RuntimeException("Network error: ${nsError.localizedDescription}"))
return@dataTaskWithRequest
}
val httpResponse = response as? NSHTTPURLResponse
if (httpResponse == null || httpResponse.statusCode.toInt() !in 200..299) {
val statusCode = httpResponse?.statusCode?.toInt() ?: 0
val responseBody = data?.let { NSString.create(it, NSUTF8StringEncoding) } ?: "No response body"
close(RuntimeException("HTTP error $statusCode: $responseBody"))
return@dataTaskWithRequest
}
if (data == null) {
close(RuntimeException("Empty response body"))
return@dataTaskWithRequest
}
// 解析 SSE 流式响应
val responseString = NSString.create(data, NSUTF8StringEncoding) as String
val lines = responseString.split("\n")
for (line in lines) {
if (line.startsWith("data: ")) {
val dataLine = line.removePrefix("data: ").trim()
if (dataLine == "[DONE]") {
close()
break
}
if (dataLine.isNotEmpty()) {
try {
val jsonData = dataLine.toNSString().dataUsingEncoding(NSUTF8StringEncoding)!!
val json = NSJSONSerialization.JSONObjectWithData(jsonData, 0, null) as? Map<Any?, Any?>
val choices = json?.get("choices") as? List<*>
val firstChoice = choices?.firstOrNull() as? Map<Any?, Any?>
val delta = firstChoice?.get("delta") as? Map<Any?, Any?>
val content = delta?.get("content") as? String
if (content != null && content.isNotEmpty()) {
trySend(content)
}
} catch (e: Throwable) {
// 忽略单个数据块的解析错误,继续处理后续数据
continue
}
}
}
}
close()
} catch (e: Throwable) {
close(e)
}
}
task.resume()
// 确保任务在流取消时被取消
awaitClose {
task.cancel()
}
}
actual override suspend fun chatCompletionsBlocking(
messages: List<ChatMessage>,
model: String,
temperature: Double
): String = suspendCancellableCoroutine { continuation ->
val url = NSURL.URLWithString("https://api.openai.com/v1/chat/completions")!!
val request = NSMutableURLRequest.requestWithURL(url)
request.setHTTPMethod("POST")
request.setValue("Bearer $apiKey", forHTTPHeaderField = "Authorization")
request.setValue("application/json", forHTTPHeaderField = "Content-Type")
val requestBody = NSMutableDictionary()
requestBody.setObject(model, forKey = "model")
requestBody.setObject(messages.map { it.toMap() }, forKey = "messages")
requestBody.setObject(temperature, forKey = "temperature")
requestBody.setObject(false, forKey = "stream")
do {
val jsonData = try {
NSJSONSerialization.dataWithJSONObject(requestBody, 0, null)
} catch (e: NSException) {
continuation.resumeWithException(IllegalArgumentException("Failed to serialize request body", e))
break
}
request.HTTPBody = jsonData
val task = NSURLSession.sharedSession.dataTaskWithRequest(request) { data, response, error ->
try {
if (error != null) {
val nsError = error as NSError
continuation.resumeWithException(RuntimeException("Network error: ${nsError.localizedDescription}"))
return@dataTaskWithRequest
}
val httpResponse = response as? NSHTTPURLResponse
if (httpResponse == null || httpResponse.statusCode.toInt() !in 200..299) {
val statusCode = httpResponse?.statusCode?.toInt() ?: 0
val responseBody = data?.let { NSString.create(it, NSUTF8StringEncoding) } ?: "No response body"
continuation.resumeWithException(RuntimeException("HTTP error $statusCode: $responseBody"))
return@dataTaskWithRequest
}
if (data == null) {
continuation.resumeWithException(RuntimeException("Empty response body"))
return@dataTaskWithRequest
}
val jsonString = NSString.create(data, NSUTF8StringEncoding) as String
val json = try {
NSJSONSerialization.JSONObjectWithData(data, 0, null) as? Map<Any?, Any?>
} catch (e: NSException) {
continuation.resumeWithException(RuntimeException("Failed to parse JSON response", e))
return@dataTaskWithRequest
}
val choices = json?.get("choices") as? List<*>
val firstChoice = choices?.firstOrNull() as? Map<Any?, Any?>
val message = firstChoice?.get("message") as? Map<Any?, Any?>
val content = message?.get("content") as? String
if (content != null) {
continuation.resume(content)
} else {
continuation.resumeWithException(RuntimeException("Invalid response format: $jsonString"))
}
} catch (e: Throwable) {
continuation.resumeWithException(e)
}
}
task.resume()
continuation.invokeOnCancellation {
task.cancel()
}
} while (false)
}
private fun ChatMessage.toMap(): Map<String, Any> {
return mapOf("role" to role, "content" to content)
}
}
3. 架构图描述
以下 Mermaid 流程图描述了上述 AIService 在 KMP 全栈 AI Agent 中的架构位置与数据流:
架构说明:
- 共享模块 :包含 AI Agent 的核心工作流、记忆管理和工具调用逻辑。它依赖
AIService接口来与 AI 模型交互,但不知道具体实现。 - 平台实现层 :各平台(Android、iOS、JVM)提供
AIService的actual实现,处理平台特定的网络、安全策略(如证书锁定)和性能优化。 - 外部 AI 服务:实现层最终调用 OpenAI API 或集成本地 LangChain/Kotlin 运行时。共享模块的业务逻辑可以无缝切换不同的 AI 后端。
4. 在共享业务逻辑中使用 AIService
定义好接口和实现后,在共享模块中就可以编写与平台无关的 AI Agent 逻辑了。
kotlin
// commonMain/kotlin/com/example/agent/EmailAgent.kt
class EmailClassificationAgent(private val aiService: AIService) {
suspend fun classifyAndReply(emailContent: String): String {
val systemPrompt = "你是一个邮件分类助手,请判断邮件类型并生成简要回复。"
val userMessage = "邮件内容:$emailContent"
val messages = listOf(
ChatMessage("system", systemPrompt),
ChatMessage("user", userMessage)
)
// 调用共享的 AI 服务,实际执行由平台决定
val reply = aiService.chatCompletionsBlocking(messages)
return reply
}
}
// 在共享模块中可以通过依赖注入或工厂来获取平台特定的 AIService 实例
// 例如在 Android 的 ViewModel 或 iOS 的 ViewController 中初始化 EmailClassificationAgent
概念验证:智能邮件分类与回复助手的 KMP 全栈设计
基于上述 AIService 封装,我们可以设计一个全栈的"智能邮件分类与回复助手"Agent:
- 共享模块 :包含
EmailClassificationAgent类(如上)、邮件解析逻辑、分类规则和回复模板。 - Android/iOS 移动端 :通过
AIService的实际实现调用云端或本地 AI 模型,在移动设备上提供即时邮件分类和草稿回复功能,并可集成系统通知。 - JVM 服务端 :将相同的
EmailClassificationAgent部署为后端服务,处理批量邮件或为其他客户端提供 API。由于共享了核心逻辑,移动端和服务端的分类行为完全一致。 - 桌面端:使用 Compose Multiplatform 构建跨平台桌面 UI,复用相同的 Agent 逻辑,提供更强大的邮件管理和编辑界面。
通过这种设计,AI 能力的集成点被统一在 AIService 接口,各平台只需关注网络和交互细节,而 Agent 的"大脑"(业务逻辑)则在 KMP 共享模块中一次编写,处处运行。
七、 挑战、最佳实践与学习路径
- 面临的挑战: 包体积、原生互操作复杂性、社区资源相对较少、调试难度。
- 最佳实践建议: 渐进式采用、清晰的模块边界、充分的测试(共享测试)、CI/CD 流水线。
- 推荐学习路径: Kotlin 基础 → KMP 核心概念 → 移动端实践 → 后端 Ktor → 全栈项目 → 探索 AI 集成。
- 资源推荐: 官方文档、开源项目、社区论坛、相关会议与文章。
八、 总结与展望
总结 KMP 实现全栈开发的核心价值,并展望其在云原生、边缘计算、物联网(IoT)以及更智能的 AI Agent 领域的潜在应用,鼓励开发者拥抱这一技术趋势。