鸿蒙聊天 Demo 练习 01:发送消息、模拟 AI 回复与自动滚动
一、本次分支
bash
feature/chat-auto-scroll-loading
二、本次目标
本次在聊天 Demo 的基础页面上,完善了一个最小可用的聊天流程:
- 用户输入消息
- 点击发送
- 消息追加到聊天数组
- 模拟 AI 延迟回复
- 消息列表自动滚动到底部
- 发送期间禁用输入框和按钮,避免重复发送
三、涉及文件
text
entry/src/main/ets/pages/Setting.ets
四、页面结构
当前聊天页面分为三部分:
text
Column
├── Header 顶部标题栏
├── MessageList 中间消息列表
└── InputBar 底部输入栏
对应代码:
ts
build() {
Column() {
this.Header()
this.MessageList()
this.InputBar()
}
}
这种结构非常适合聊天页面:
- 顶部固定标题
- 中间区域使用
layoutWeight(1)占据剩余空间 - 底部输入框固定在页面底部
五、消息数据结构
ts
interface ChatItem {
id: number
type: 'user' | 'ai'
content: string
}
字段说明:
| 字段 | 作用 |
|---|---|
| id | 唯一标识,用于 ForEach 的 key |
| type | 区分用户消息和 AI 消息 |
| content | 消息文本内容 |
六、为什么消息数组使用 concat
发送消息时使用:
ts
this.chatList = this.chatList.concat([userItem])
而不是:
ts
this.chatList.push(userItem)
原因是 concat 会返回一个新数组,更容易触发 ArkUI 的状态更新。
简单理解:
text
push:修改原数组
concat:生成新数组,然后重新赋值
在响应式 UI 中,重新赋值通常更稳定。
七、Scroller 的作用
本次用到了:
ts
private listScroller: Scroller = new Scroller()
然后绑定到 List:
ts
List({ scroller: this.listScroller }) {
...
}
这样就可以通过代码控制列表滚动:
ts
this.listScroller.scrollToIndex(this.chatList.length)
八、为什么滚动到底部要用 setTimeout
消息数组更新后,UI 不一定立刻渲染完成。
如果马上滚动,可能会出现滚动不到最新消息的问题。
所以封装了一个方法:
ts
scrollToBottom(): void {
setTimeout(() => {
this.listScroller.scrollToIndex(this.chatList.length)
}, 50)
}
逻辑是:
text
先更新数组
等待 UI 刷新
再滚动到底部
九、为什么滚动索引是 chatList.length
List 中除了消息列表,还有一个底部占位项:
ts
ListItem() {
Row() {
Blank()
}
.height(12)
}
假设有 3 条消息:
text
索引 0:第一条消息
索引 1:第二条消息
索引 2:第三条消息
索引 3:底部占位项
所以:
ts
this.listScroller.scrollToIndex(this.chatList.length)
刚好可以滚到最后的底部占位项。
十、模拟 AI 回复
本次没有真正调用接口,而是使用 setTimeout 模拟异步请求:
ts
mockAskAi(question: string): void {
setTimeout(() => {
const aiItem: ChatItem = {
id: this.nextId++,
type: 'ai',
content: `你刚才说的是:${question}`
}
this.chatList = this.chatList.concat([aiItem])
this.isSending = false
this.scrollToBottom()
}, 600)
}
以后真正接接口时,可以把 setTimeout 替换成网络请求。
十一、发送中状态
定义状态:
ts
@Local isSending: boolean = false
发送时:
ts
this.isSending = true
AI 回复完成后:
ts
this.isSending = false
按钮根据状态变化:
ts
Button(this.isSending ? '发送中' : '发送')
.enabled(!this.isSending)
输入框也可以禁用:
ts
.enabled(!this.isSending)
十二、本次知识点总结
本次练习涉及以下鸿蒙开发知识点:
@ComponentV2组件写法@Local本地响应式状态Column / Row / List / ListItem / TextInput / ButtonForEach渲染数组Scroller控制列表滚动scrollToIndex滚动到指定位置setTimeout处理 UI 渲染后的延迟滚动KeyboardAvoidMode.RESIZE处理键盘顶起页面- 使用
concat更新数组,保证状态刷新 - 使用
isSending控制按钮禁用和加载状态
十三、面试表达
这个功能可以这样说:
我在聊天 Demo 中实现了基础的消息发送和模拟 AI 回复流程。页面采用上中下布局,中间使用
List渲染消息数组,底部使用TextInput和Button处理输入。为了保证发送消息后能自动看到最新内容,我创建了Scroller实例并绑定到List,在消息数组更新后通过scrollToIndex滚动到底部。同时考虑到 UI 渲染存在时序问题,所以封装了scrollToBottom方法,用setTimeout延迟滚动,保证列表刷新后再执行滚动。另外我还加入了isSending状态,模拟接口请求期间禁用输入和按钮,避免重复发送。
十四、本次提交命令
bash
git add entry/src/main/ets/pages/Setting.ets docs/01-chat-auto-scroll-loading.md
git commit -m "feat: add chat auto scroll and loading state"
git push origin feature/chat-auto-scroll-loading
十五、本次练习总结
这一小节的重点不是聊天功能本身,而是理解一个聊天页面最基础的状态流转:
text
用户输入
↓
点击发送
↓
追加用户消息
↓
模拟请求中状态
↓
追加 AI 回复
↓
滚动到底部
这套流程后面可以继续扩展成:
- 打字机输出
- 真实接口请求
- 本地历史记录
- 会话列表
- 消息组件拆分
- 输入框聚焦和键盘处理