一、项目背景与架构设计

在跨端 IM 场景里,最难的往往不是“能不能跑”,而是消息列表是否跟手、键盘是否顺滑、各端体验是否一致。Kuikly 的优势在于:UI 与业务逻辑全部用 Kotlin 编写,直出各端原生视图,无 JS Bridge 开销,非常适合聊天页这种高频交互场景。

项目采用 KMM + Kuikly DSL​ 的分层结构,核心原则是:

UI + 业务逻辑全部收敛在 commonMain,只有极少数平台差异(时间、存储、录音等)用 expect/actual 分流。

shared/
├── commonMain/        # ✅ 核心:UI + 业务 + 数据层
├── androidMain/       # Android 平台差异实现
├── iosMain/           # iOS 平台差异实现
├── ohosArm64Main/     # 鸿蒙平台差异实现
└── jsMain/            # H5 / 小程序差异实现

二、数据层:消息模型与状态驱动

聊天应用的核心是消息流,我们用 kotlinx.serialization 定义统一的数据模型,并用 Kuikly 的响应式 API 驱动 UI 自动更新。

// commonMain — data/Message.kt
@Serializable
data class Message(
    val id: String,
    val text: String,
    val senderId: String,
    val timestamp: Long,
    val status: MessageStatus = MessageStatus.SENDING
)

@Serializable
enum class MessageStatus { SENDING, SENT, FAILED }

在 ChatPage 中,使用 observableList 管理消息列表,新增消息时 UI 自动刷新,无需手动 notifyItemChanged:

private val currentUserId = "me"
private var messages by observableList<Message>()
private var inputText by observable("")

三、核心 UI:消息列表 + 气泡

1. 高性能消息列表

聊天页对性能要求极高。Kuikly 的 List + vfor 内部做了差分更新和视图复用,非常适合长列表:

List {
    attr {
        flex(1f)
        paddingHorizontal(12f)
    }
    vfor({ messages }) { msg ->
        MessageBubble(msg, msg.senderId == currentUserId)
    }
}

2. 左右气泡布局

通过 senderId 判断消息归属,控制对齐方向与气泡颜色:

fun ViewContainer<*, *>.MessageBubble(msg: Message, isMine: Boolean) {
    View {
        attr {
            flexDirectionRow()
            if (isMine) justifyContentFlexEnd() else justifyContentFlexStart()
            marginVertical(4f)
        }
        View {
            attr {
                maxWidth(75f)
                padding(10f)
                borderRadius(12f)
                backgroundColor(if (isMine) Color(0xFF4A90D9) else Color.WHITE)
            }
            Text {
                attr {
                    text(msg.text)
                    fontSize(15f)
                    color(if (isMine) Color.WHITE else Color(0xFF333333))
                }
            }
        }
    }
}

四、输入栏与键盘适配

1. 输入栏组件

输入栏包含 Input 和发送按钮,通过回调将事件抛给 ChatPage:

InputBar(
    text = { inputText },
    onTextChange = { inputText = it },
    onSend = { sendMessage() }
)

2. 键盘弹起处理(关键体验点)

Kuikly 的 Input 在各端原生渲染,键盘自动弹起。为了让输入栏跟随键盘上移,我们监听键盘高度:

private var keyboardHeight by observable(0f)

override fun created() {
    acquireModule<KRKeyboardModule>(KRKeyboardModule.MODULE_NAME)
        .addKeyboardHeightListener { params ->
            keyboardHeight = params.height
        }
}

并在输入栏上动态设置 marginBottom:

attr { marginBottom(keyboardHeight) }

这样在 Android / iOS / 鸿蒙上都能获得原生级键盘跟随体验。


五、网络层:WebSocket + HTTP

聊天应用通常采用 WebSocket 长连接 + HTTP 兜底​ 的架构。

class ChatRepository(
    private val onNewMessage: (Message) -> Unit
) {
    fun connect() {
        // 通过 KRWebSocketModule 建立长连接
        isConnected = true
    }

    fun send(msg: Message) {
        CoroutineScope(Dispatchers.Default).launch {
            // KRHttpModule.post("/api/messages", json.encodeToString(msg))
        }
    }

    fun loadHistory(onLoaded: (List<Message>) -> Unit) {
        CoroutineScope(Dispatchers.Default).launch {
            // 拉取历史消息并回调
        }
    }
}

在 ChatPage.created() 中初始化连接和拉取历史:

repository.connect()
repository.loadHistory { history ->
    messages.addAll(0, history)
}

六、平台差异处理:expect / actual

典型差异点如时间格式化,通过 expect/actual 在各端分别实现:

// commonMain
expect fun formatTimestamp(ts: Long): String

// androidMain
actual fun formatTimestamp(ts: Long): String {
    return SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(ts))
}

// jsMain
actual fun formatTimestamp(ts: Long): String {
    val date = kotlinx.browser.Date(ts.toDouble())
    return "${date.getHours()}:${date.getMinutes()}"
}

建议优先使用 kotlinx-datetime 等纯 Kotlin 库,减少手写 expect/actual 的成本。


七、Compose DSL 对照(可选)

如果团队更熟悉 Jetpack Compose,也可以直接使用 Kuikly 的 Compose DSL,跨端能力完全一致:

@Composable
fun ChatPage() {
    var messages by remember { mutableStateOf(listOf<Message>()) }
    var inputText by remember { mutableStateOf("") }

    Column(modifier = Modifier.fillMaxSize()) {
        LazyColumn(modifier = Modifier.weight(1f)) {
            items(messages) { msg ->
                MessageBubble(msg, msg.senderId == "me")
            }
        }
        Row(modifier = Modifier.padding(8.dp)) {
            TextField(value = inputText, onValueChange = { inputText = it })
            Button(onClick = { /* send */ }) { Text("发") }
        }
    }
}

八、关键实践总结

关注点

实践建议

列表性能

使用 List + vfor / LazyColumn,依赖框架差分更新

图片消息

使用 Image { attr { src(uri) } },各端自动解码

语音消息

通过 expect/actual 或 Module 桥接原生录音与播放

离线缓存

进入页面拉历史,退出时通过 expect/actual 缓存到本地

消息状态

用 Message.status 驱动 UI(发送中 / 已送达 / 失败)

推送通知

各端分别实现 PushModule,接入 APNs / FCM / 华为推送


九、一句话总结

Kuikly 做聊天 App 的核心竞争力是:一套 Kotlin 代码写 UI + 业务,直出各端原生视图,无 JS Bridge 转译,消息列表滚动和键盘交互体验接近原生。

这套方案已经在多个生产级 IM 项目中落地,既保留了 Kotlin 的开发效率,又满足了 各端对性能和体验的严苛要求,是构建跨端聊天应用的理想选择。

Logo

一站式 AI 云服务平台

更多推荐