引言:当跨端开发遇见流式 AI

在当前 AI 应用蓬勃发展的时代,流畅的流式输出体验已成为提升用户粘性的关键因素。然而,为 Android、iOS、鸿蒙等多个平台分别开发功能相同、体验一致的 AI 应用,不仅成本高昂,而且维护困难。

Kuikly——腾讯开源的基于 Kotlin MultiPlatform (KMP) 的跨端开发框架,为此提供了完美的解决方案。本文将深入演示如何利用 Kuikly 的核心特性,构建一个能在所有主流平台上提供原生性能、极致流畅的 AI 流式对话应用。

一、核心架构设计:Kuikly 的独特优势

在深入编码实现之前,我们先分析为何 Kuikly 特别适合此类场景:

核心技术优势

  1. 真正的逻辑跨端

    • AI 对话管理、状态控制、网络请求等核心业务逻辑只需用 Kotlin 编写一次
    • 无需为不同平台重复实现相同业务逻辑
  2. 响应式 UI 更新

    • 内置响应式系统与 AI 流式输出的"逐字出现"特性完美契合
    • 实现最低延迟的 UI 更新,提供丝滑用户体验
  3. 灵活的 Module 机制

    • 优雅封装平台特定功能(网络请求、文件访问等)
    • 为 Kotlin 业务层提供统一调用接口
  4. 原生渲染性能

    • UI 由各平台原生组件渲染,无自绘引擎开销
    • 确保滚动、动画等交互如原生应用般流畅

应用架构设计

我们的应用采用清晰的三层架构:

┌─────────────────────────────────────────┐
│           Kuikly UI 层 (Kotlin)          │
│   负责聊天界面渲染和用户交互              │
├─────────────────────────────────────────┤
│          业务逻辑层 (Kotlin)             │
│   管理对话状态、调用AI服务、处理流式数据  │
├─────────────────────────────────────────┤
│          平台适配层 (Module)             │
│   处理网络请求等平台相关操作              │
└─────────────────────────────────────────┘

二、核心实现:业务逻辑与 UI 构建

所有跨端逻辑均在 shared 模块中实现。

1. 数据模型定义

// 消息类型枚举
enum class MessageType {
    USER, AI, SYSTEM
}

// 消息状态枚举
enum class MessageStatus {
    SENDING, SUCCESS, ERROR, STREAMING
}

// 单条消息数据类
data class ChatMessage(
    val id: String = generateId(),
    val type: MessageType,
    var content: String,
    val timestamp: Long = System.currentTimeMillis(),
    var status: MessageStatus = MessageStatus.SUCCESS
) {
    companion object {
        fun createUserMessage(content: String): ChatMessage {
            return ChatMessage(type = MessageType.USER, content = content)
        }

        fun createAIStreamingMessage(): ChatMessage {
            return ChatMessage(type = MessageType.AI, content = "", status = MessageStatus.STREAMING)
        }
    }
}

// 对话会话数据类
data class ChatSession(
    val id: String,
    val title: String,
    val messages: ObservableList<ChatMessage> = observableListOf(),
    val createTime: Long = System.currentTimeMillis()
)

2. AI 服务管理核心

这是流式处理的核心模块,通过 Module 机制保证跨平台兼容性:

import com.tencent.kuikly.core.module.Module
import org.json.JSONObject

// 定义AI服务Module抽象类
abstract class AIServiceModule : Module() {
    companion object {
        const val MODULE_NAME = "AIServiceModule"
    }

    // 流式响应回调接口
    interface StreamCallback {
        fun onChunkReceived(chunk: String, isLast: Boolean)
        fun onError(error: String)
    }

    // 发起流式对话请求的抽象方法
    abstract fun streamChat(
        messages: List<ChatMessage>,
        apiKey: String,
        callback: StreamCallback
    )
}

// 辅助函数:获取AI服务实例
fun acquireAIService(): AIServiceModule {
    return acquireModule(AIServiceModule.MODULE_NAME) as AIServiceModule
}

// 对话管理的核心ViewModel
class ChatViewModel {
    private val _currentSession = observable<ChatSession?>(null)
    private val _isLoading = observable(false)

    // 发送消息并接收流式响应
    fun sendUserMessage(userInput: String) {
        val userMessage = ChatMessage.createUserMessage(userInput)
        _currentSession.value?.messages?.add(userMessage)

        // 创建初始AI消息用于流式更新
        val aiMessage = ChatMessage.createAIStreamingMessage()
        _currentSession.value?.messages?.add(aiMessage)

        _isLoading.value = true

        // 准备对话历史
        val history = _currentSession.value?.messages?.filter { 
            it.status == MessageStatus.SUCCESS 
        } ?: emptyList()

        // 通过Module调用平台网络库
        acquireAIService().streamChat(
            messages = history,
            apiKey = "your_api_key",
            object : AIServiceModule.StreamCallback {
                private val stringBuilder = StringBuilder()

                override fun onChunkReceived(chunk: String, isLast: Boolean) {
                    // 关键:在Kuikly线程更新UI
                    runOnKuiklyThread {
                        stringBuilder.append(chunk)
                        // 直接更新AI消息content,响应式系统自动刷新UI
                        aiMessage.content = stringBuilder.toString()

                        if (isLast) {
                            aiMessage.status = MessageStatus.SUCCESS
                            _isLoading.value = false
                        }
                    }
                }

                override fun onError(error: String) {
                    runOnKuiklyThread {
                        aiMessage.status = MessageStatus.ERROR
                        aiMessage.content = "错误:$error"
                        _isLoading.value = false
                    }
                }
            }
        )
    }
}

3. 流式聊天界面实现

使用 Kuikly DSL 构建响应式聊天界面:

import com.tencent.kuikly.core.annotation.Page
import com.tencent.kuikly.core.pager.Pager
import com.tencent.kuikly.core.viewbuilder.*
import com.tencent.kuikly.core.viewbuilder.view.*

@Page("AIChat")
class StreamChatPage : Pager() {

    // 响应式数据
    private val viewModel = ChatViewModel()
    private var inputText by observable("")

    override fun body(): ViewBuilder {
        return {
            // 整体垂直布局
            Column {
                attr {
                    width(matchParent)
                    height(matchParent)
                    backgroundColor(Color.White)
                }

                // 消息列表区域
                buildMessageList()
                
                // 底部输入区域
                buildInputArea()
                
                // 加载指示器
                buildLoadingIndicator()
            }
        }
    }

    // 构建消息列表
    private fun ViewBuilder.buildMessageList(): ViewBuilder {
        return {
            ListView {
                attr {
                    width(matchParent)
                    height(0, weight = 1f)
                    dataSource(viewModel.currentSession?.messages ?: observableListOf())
                    divider { color(Color.LightGray).height(0.5f) }
                }

                itemBuilder { message: ChatMessage, index: Int ->
                    MessageBubble(message)
                }
            }
        }
    }

    // 构建输入区域
    private fun ViewBuilder.buildInputArea(): ViewBuilder {
        return {
            Row {
                attr {
                    width(matchParent)
                    height(wrapContent)
                    padding(16f)
                    alignItems(Center)
                    backgroundColor(Color.F5F5F5)
                }

                // 文本输入框
                TextInput {
                    attr {
                        width(0, weight = 1f)
                        height(wrapContent)
                        hint("输入您的问题...")
                        text(inputText)
                        onTextChanged { newText ->
                            inputText = newText
                        }
                        borderRadius(20f)
                        backgroundColor(Color.White)
                        padding(horizontal = 16f, vertical = 12f)
                    }
                }

                // 发送按钮
                Text {
                    attr {
                        text("发送")
                        fontSize(16f)
                        color(if (inputText.isNotEmpty()) Color.Blue else Color.Gray)
                        padding(horizontal = 20f, vertical = 12f)
                        margin(left = 12f)
                        onClick {
                            if (inputText.isNotEmpty()) {
                                viewModel.sendUserMessage(inputText)
                                inputText = "" // 清空输入框
                            }
                        }
                    }
                }
            }
        }
    }

    // 构建加载指示器
    private fun ViewBuilder.buildLoadingIndicator(): ViewBuilder {
        return {
            If(viewModel.isLoading) {
                Column {
                    attr {
                        width(matchParent)
                        height(wrapContent)
                        alignItems(Center)
                        padding(16f)
                    }

                    Text {
                        attr {
                            text("AI 正在思考...")
                            fontSize(14f)
                            color(Color.Gray)
                        }
                    }

                    // 加载动画
                    Row {
                        attr {
                            width(wrapContent)
                            height(wrapContent)
                            margin(top = 8f)
                        }

                        repeat(3) { index ->
                            Text {
                                attr {
                                    text(".")
                                    fontSize(18f)
                                    color(Color.Blue)
                                    margin(horizontal = 2f)
                                    animation(
                                        delay = index * 200L,
                                        duration = 1000,
                                        repeatCount = Infinite
                                    ) {
                                        scale(1.5f)
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    // 消息气泡组件
    private fun ViewBuilder.MessageBubble(message: ChatMessage): ViewBuilder {
        return {
            val isUser = message.type == MessageType.USER

            Row {
                attr {
                    width(matchParent)
                    height(wrapContent)
                    padding(horizontal = 16f, vertical = 8f)
                    if (!isUser) {
                        justifyContent(FlexStart)
                    } else {
                        justifyContent(FlexEnd)
                    }
                }

                Column {
                    attr {
                        width(0, weight = 0.7f)
                        height(wrapContent)
                        padding(horizontal = 12f, vertical = 8f)
                        borderRadius(12f)
                        backgroundColor(if (isUser) Color.Blue else Color.LightGray)
                        alignItems(if (isUser) FlexEnd else FlexStart)
                    }

                    // 消息内容
                    Text {
                        attr {
                            text(message.content)
                            fontSize(16f)
                            color(if (isUser) Color.White else Color.Black)
                            lineHeight(24f)
                        }
                    }

                    // 流式输出光标动画
                    If(message.status == MessageStatus.STREAMING) {
                        Text {
                            attr {
                                text("▋")
                                fontSize(16f)
                                color(if (isUser) Color.White else Color.Black)
                                margin(top = 2f)
                                animation(duration = 600, repeatCount = Infinite) {
                                    opacity(0f)
                                }
                            }
                        }
                    }

                    // 时间戳
                    Text {
                        attr {
                            text(formatTime(message.timestamp))
                            fontSize(12f)
                            color(if (isUser) Color.WhiteAlpha(0.7) else Color.Gray)
                            margin(top = 4f)
                        }
                    }
                }
            }
        }
    }

    private fun formatTime(timestamp: Long): String {
        return android.text.format.DateFormat.format("HH:mm", timestamp).toString()
    }
}

三、平台适配:AI Service Module 实现

由于各平台网络库存在差异,我们需要分别实现 AIServiceModule。

Android 端实现

// 在 androidApp 模块中
class AndroidAIServiceModule : AIServiceModule() {

    override fun streamChat(
        messages: List<ChatMessage>, 
        apiKey: String, 
        callback: AIServiceModule.StreamCallback
    ) {
        // 使用 OkHttp 实现流式请求
        val client = OkHttpClient()
        val request = Request.Builder()
            .url("https://api.openai.com/v1/chat/completions")
            .addHeader("Authorization", "Bearer $apiKey")
            .post(createRequestBody(messages))
            .build()

        client.newCall(request).enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                response.body?.let { body ->
                    val source = body.source()
                    while (!source.exhausted()) {
                        val line = source.readUtf8Line() ?: break
                        if (line.startsWith("data: ")) {
                            val json = line.removePrefix("data: ")
                            if (json == "[DONE]") {
                                callback.onChunkReceived("", true)
                                return
                            }
                            val chunk = parseChunk(json)
                            callback.onChunkReceived(chunk, false)
                        }
                    }
                    callback.onChunkReceived("", true)
                }
            }

            override fun onFailure(call: Call, e: IOException) {
                callback.onError(e.message ?: "Network error")
            }
        })
    }
}

// 在 KuiklyRenderActivity 中注册模块
private fun initKuiklyAdapter() {
    with(KuiklyRenderAdapterManager) {
        moduleExport(AIServiceModule.MODULE_NAME) {
            AndroidAIServiceModule()
        }
    }
}

iOS 端实现

// 在 iOSApp 中
class IOSAIServiceModule: AIServiceModule {
    
    override func streamChat(
        messages: [ChatMessage], 
        apiKey: String, 
        callback: AIServiceModuleStreamCallback
    ) {
        // 使用 URLSession 实现流式请求
        var request = URLRequest(url: URL(string: "https://api.openai.com/v1/chat/completions")!)
        request.httpMethod = "POST"
        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.httpBody = createRequestBody(messages)
        
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            // 处理流式响应逻辑
            // 与Android端类似,使用平台特定的流式处理方式
        }
        task.resume()
    }
}

// 注册到Kuikly框架
KuiklyRenderAdapterManager.shared.registerModule(
    name: AIServiceModule.MODULE_NAME, 
    factory: { IOSAIServiceModule() }
)

四、性能优化与最佳实践

关键优化策略

  1. 线程安全保证

    • 确保所有流式回调通过 runOnKuiklyThread 更新 UI
    • 避免多线程环境下的UI更新冲突
  2. 内存管理优化

    • 及时取消进行中的网络请求
    • 在页面销毁时清理相关资源,防止内存泄漏
  3. 用户体验增强

    • 对用户快速连续点击实施防抖处理
    • 使用 Kuikly 的 StorageModule 缓存对话历史
    • 实现完善的错误处理和重试机制
  4. 性能监控

    • 监控流式输出的响应延迟
    • 优化大消息列表的渲染性能

五、成果展示与价值

完成上述实现后,您将获得一个真正跨端的 AI 聊天应用:

跨端一致性

  • Android 端:使用原生 Android 组件渲染,滚动流畅自然
  • iOS 端:基于原生 UIKit 组件,完美符合 iOS 设计规范
  • 鸿蒙端:通过 ArkUI 原生渲染,提供丝滑操作体验

开发效率提升

  • 业务逻辑 100% 复用:核心对话逻辑只需编写一次
  • UI 表现高度一致:三端用户体验统一
  • 流式输出效果卓越:"逐字打印"效果在所有平台都具备原生般的流畅度

结语

通过 Kuikly 框架,我们不仅实现了"一次编写,多端运行"的开发效率革命,更重要的是能够以统一的技术栈和架构思想,为所有平台用户提供最高质量的 AI 应用体验。这种将前沿 AI 能力与成熟跨端技术深度结合的模式,正代表着未来应用开发的主流方向。

Kuikly 的 Module 机制和响应式系统为复杂交互场景提供了坚实的技术支撑,让开发者能够专注于创造更优秀的 AI 体验,而不是陷入多端适配的技术泥潭。随着 AI 技术的不断演进和跨端方案的持续成熟,这种开发模式将在未来的应用生态中扮演越来越重要的角色。


进一步探索

  • 尝试集成不同的 AI 服务提供商
  • 实现更复杂的对话管理功能(上下文管理、对话持久化等)
  • 探索 Kuikly 在其他 AI 场景中的应用(图像生成、语音交互等)
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐