暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

109.[HarmonyOS NEXT 实战案例:聊天应用] 进阶篇 - 交互功能与状态管理

原创 若城 2025-06-09
106

[HarmonyOS NEXT 实战案例:聊天应用] 进阶篇 - 交互功能与状态管理

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

引言

在基础篇中,我们学习了如何使用HarmonyOS NEXT的ColumnSplit组件构建聊天应用的基本布局。本篇教程将进一步深入,讲解如何为聊天应用添加交互功能和状态管理,包括消息发送、联系人切换、消息状态更新等功能,使界面更加动态和交互友好。

状态管理概述

在聊天应用中,我们需要管理多种状态,包括:

状态类型 描述 实现方式
消息列表 存储当前聊天的所有消息 @State messages: Message[]
联系人列表 存储所有联系人信息 @State contacts: Contact[]
当前联系人 记录当前选中的联系人 @State currentContact: number
新消息输入 存储用户正在输入的消息 @State newMessage: string
消息发送状态 记录消息是否正在发送 @State isSending: boolean

这些状态的变化会直接影响界面的显示和交互行为。通过合理的状态管理,我们可以实现流畅的用户体验。

交互功能实现

1. 联系人切换功能

当用户点击联系人列表中的某个联系人时,我们需要切换当前聊天的联系人,并显示与该联系人的聊天记录。

// 联系人项点击事件 .onClick(() => { this.currentContact = contact.id // 模拟加载与该联系人的聊天记录 this.loadChatHistory(contact.id) }) // 加载聊天记录的方法 private loadChatHistory(contactId: number) { // 在实际应用中,这里应该从数据库或服务器加载聊天记录 // 这里使用模拟数据 if (contactId === 1) { this.messages = [ { id: 1, content: '你好!最近怎么样?', time: '10:30', isMe: false }, { id: 2, content: '我很好,谢谢关心!你呢?', time: '10:32', isMe: true }, { id: 3, content: '我也还不错。周末有空一起吃饭吗?', time: '10:33', isMe: false }, { id: 4, content: '好啊,周六中午怎么样?', time: '10:35', isMe: true }, ] } else if (contactId === 2) { this.messages = [ { id: 1, content: '项目进展如何?', time: '昨天', isMe: false }, { id: 2, content: '已经完成了大部分功能,还有一些细节需要调整', time: '昨天', isMe: true }, { id: 3, content: '项目文档已经发给你了', time: '昨天', isMe: false }, ] } else { this.messages = [ { id: 1, content: '下周会议时间确定了吗?', time: '周一', isMe: false }, { id: 2, content: '还没有,等确定了我会通知你', time: '周一', isMe: true }, ] } }

在这段代码中,我们为联系人项添加了点击事件,点击时更新当前选中的联系人ID,并调用loadChatHistory方法加载与该联系人的聊天记录。在实际应用中,这个方法应该从数据库或服务器加载真实的聊天记录,这里我们使用模拟数据进行演示。

2. 消息发送功能

当用户输入消息并点击发送按钮时,我们需要将新消息添加到消息列表中,并更新联系人列表中的最后一条消息。

// 发送按钮点击事件 .onClick(() => { if (this.newMessage.trim() !== '') { // 添加新消息到消息列表 const newMsg = { id: this.messages.length + 1, content: this.newMessage, time: this.getCurrentTime(), isMe: true } this.messages.push(newMsg) // 更新联系人列表中的最后一条消息 this.updateContactLastMessage(this.currentContact, newMsg) // 清空输入框 this.newMessage = '' // 模拟对方回复 this.simulateReply() } }) // 更新联系人最后一条消息的方法 private updateContactLastMessage(contactId: number, message: Message) { const index = this.contacts.findIndex(contact => contact.id === contactId) if (index !== -1) { this.contacts[index] = { ...this.contacts[index], lastMessage: message.content, time: message.time, unread: 0 } } } // 模拟对方回复的方法 private simulateReply() { // 设置发送状态为正在发送 this.isSending = true // 延迟2秒,模拟网络延迟 setTimeout(() => { // 生成随机回复内容 const replies = [ '好的,我知道了', '谢谢你的信息', '我稍后回复你', '这个想法不错', '我们需要再讨论一下' ] const randomIndex = Math.floor(Math.random() * replies.length) const replyContent = replies[randomIndex] // 添加回复消息到消息列表 const replyMsg = { id: this.messages.length + 1, content: replyContent, time: this.getCurrentTime(), isMe: false } this.messages.push(replyMsg) // 更新联系人列表中的最后一条消息 this.updateContactLastMessage(this.currentContact, replyMsg) // 设置发送状态为已完成 this.isSending = false }, 2000) }

在这段代码中,我们为发送按钮添加了点击事件,点击时检查消息是否为空,如果不为空,则将新消息添加到消息列表中,更新联系人列表中的最后一条消息,并清空输入框。然后调用simulateReply方法模拟对方回复,这个方法会在2秒后生成一条随机回复消息,并添加到消息列表中。

3. 未读消息处理

当收到新消息时,如果当前没有选中该联系人,我们需要增加该联系人的未读消息数。

// 接收新消息的方法 private receiveMessage(contactId: number, content: string) { // 创建新消息对象 const newMsg = { id: this.messages.length + 1, content: content, time: this.getCurrentTime(), isMe: false } // 如果当前正在与该联系人聊天,直接添加到消息列表 if (this.currentContact === contactId) { this.messages.push(newMsg) } else { // 否则增加未读消息数 const index = this.contacts.findIndex(contact => contact.id === contactId) if (index !== -1) { this.contacts[index] = { ...this.contacts[index], lastMessage: content, time: newMsg.time, unread: this.contacts[index].unread + 1 } } } } // 清除未读消息的方法 private clearUnread(contactId: number) { const index = this.contacts.findIndex(contact => contact.id === contactId) if (index !== -1 && this.contacts[index].unread > 0) { this.contacts[index] = { ...this.contacts[index], unread: 0 } } }

在这段代码中,我们定义了两个方法:receiveMessageclearUnreadreceiveMessage方法用于接收新消息,如果当前正在与该联系人聊天,则直接添加到消息列表;否则增加该联系人的未读消息数。clearUnread方法用于清除联系人的未读消息数,当用户点击联系人时调用。

4. 消息状态显示

我们可以为消息添加状态显示,如发送中、已发送、已读等状态。

// 扩展消息数据类型 interface Message { id: number content: string time: string isMe: boolean status?: 'sending' | 'sent' | 'read' // 消息状态:发送中、已发送、已读 } // 消息项渲染 if (message.isMe) { // 自己发送的消息 Row() { Column() { Text(message.content) .fontSize(16) .backgroundColor('#95ec69') .padding(10) .borderRadius(10) Row() { Text(message.time) .fontSize(12) .fontColor('#999999') // 显示消息状态 if (message.status === 'sending') { Text('发送中...') .fontSize(12) .fontColor('#999999') .margin({ left: 5 }) } else if (message.status === 'sent') { Text('已发送') .fontSize(12) .fontColor('#999999') .margin({ left: 5 }) } else if (message.status === 'read') { Text('已读') .fontSize(12) .fontColor('#999999') .margin({ left: 5 }) } } .margin({ top: 5 }) .alignSelf(ItemAlign.End) } .margin({ right: 10 }) } .justifyContent(FlexAlign.End) .margin({ top: 10, bottom: 10 }) }

在这段代码中,我们扩展了消息数据类型,添加了status字段表示消息状态。在消息项渲染中,我们根据消息状态显示不同的状态文本,如"发送中…"、“已发送”、"已读"等。

5. 消息输入增强

我们可以为消息输入框添加更多功能,如表情选择、图片发送等。

// 状态变量 @State isEmojiPickerVisible: boolean = false @State emojis: string[] = ['😀', '😂', '😊', '😍', '🤔', '😎', '👍', '❤️', '🎉', '🔥'] // 消息输入区域 Row() { Button($r('app.media.02')) .width(30) .height(30) .backgroundColor(Color.Transparent) .onClick(() => { this.isEmojiPickerVisible = !this.isEmojiPickerVisible }) TextInput({ text: this.newMessage, placeholder: '输入消息...' }) .height(40) .layoutWeight(1) .margin({ left: 10, right: 10 }) .borderRadius(20) .backgroundColor('#f5f5f5') .onChange((value: string) => { this.newMessage = value }) Button($r('app.media.03')) .width(30) .height(30) .backgroundColor(Color.Transparent) .onClick(() => { this.sendMessage() }) } .padding(10) .border({ width:{top:1,bottom:0,left:0,right:0}, color:'#f0f0f0', style: { top: BorderStyle.Solid } }) // 表情选择器 if (this.isEmojiPickerVisible) { Row() { ForEach(this.emojis, (emoji: string) => { Text(emoji) .fontSize(24) .padding(10) .onClick(() => { this.newMessage += emoji this.isEmojiPickerVisible = false }) }) } .padding(10) .backgroundColor('#ffffff') .border({ width:{top:1,bottom:0,left:0,right:0}, color:'#f0f0f0', style: { top: BorderStyle.Solid } }) } // 发送消息的方法 private sendMessage() { if (this.newMessage.trim() !== '') { // 创建新消息对象,状态为发送中 const newMsg = { id: this.messages.length + 1, content: this.newMessage, time: this.getCurrentTime(), isMe: true, status: 'sending' } // 添加到消息列表 this.messages.push(newMsg) // 更新联系人列表中的最后一条消息 this.updateContactLastMessage(this.currentContact, newMsg) // 清空输入框 this.newMessage = '' // 模拟消息发送过程 setTimeout(() => { // 更新消息状态为已发送 const index = this.messages.findIndex(msg => msg.id === newMsg.id) if (index !== -1) { this.messages[index] = { ...this.messages[index], status: 'sent' } } // 模拟对方已读 setTimeout(() => { // 更新消息状态为已读 const index = this.messages.findIndex(msg => msg.id === newMsg.id) if (index !== -1) { this.messages[index] = { ...this.messages[index], status: 'read' } } // 模拟对方回复 this.simulateReply() }, 2000) }, 1000) } }

在这段代码中,我们添加了表情选择器功能。点击表情按钮时,显示或隐藏表情选择器。表情选择器使用RowForEach组件显示一组预定义的表情,点击表情时将其添加到输入框中。我们还重构了发送消息的方法,添加了消息状态的模拟过程,包括发送中、已发送、已读等状态。

高级状态管理

1. 消息分组显示

在实际的聊天应用中,我们通常会按照日期对消息进行分组显示,如"今天"、“昨天”、"更早"等。

// 消息分组数据类型 interface MessageGroup { date: string messages: Message[] } // 状态变量 @State messageGroups: MessageGroup[] = [] // 加载聊天记录时进行分组 private loadChatHistory(contactId: number) { // 加载消息数据(省略) // 对消息进行分组 this.groupMessages(this.messages) } // 消息分组方法 private groupMessages(messages: Message[]) { // 清空现有分组 this.messageGroups = [] // 按日期对消息进行分组 const groups: Map<string, Message[]> = new Map() messages.forEach(message => { // 根据消息时间获取日期分组 let date = '今天' if (message.time.includes('昨天')) { date = '昨天' } else if (!message.time.includes(':')) { date = '更早' } // 将消息添加到对应的分组中 if (groups.has(date)) { groups.get(date)?.push(message) } else { groups.set(date, [message]) } }) // 将分组转换为数组 groups.forEach((messages, date) => { this.messageGroups.push({ date, messages }) }) } // 消息列表渲染 List() { ForEach(this.messageGroups, (group: MessageGroup) => { ListItem() { // 日期分隔线 Text(group.date) .fontSize(12) .fontColor('#999999') .backgroundColor('#f0f0f0') .padding(5) .borderRadius(10) .alignSelf(ItemAlign.Center) .margin({ top: 10, bottom: 10 }) } ForEach(group.messages, (message: Message) => { ListItem() { // 消息项渲染(省略) } }) }) }

在这段代码中,我们定义了消息分组数据类型MessageGroup,并添加了messageGroups状态变量。在加载聊天记录时,我们调用groupMessages方法对消息进行分组,然后在消息列表渲染中使用嵌套的ForEach组件分别渲染日期分隔线和消息项。

2. 联系人排序

我们可以根据最后消息时间对联系人进行排序,使最近有消息的联系人显示在前面。

// 联系人排序方法 private sortContacts() { // 根据最后消息时间对联系人进行排序 this.contacts.sort((a, b) => { // 将时间转换为可比较的格式 const timeA = this.convertTimeToComparable(a.time) const timeB = this.convertTimeToComparable(b.time) // 降序排序,最近的消息在前面 return timeB - timeA }) } // 将时间转换为可比较的格式 private convertTimeToComparable(time: string): number { const now = new Date() const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime() if (time.includes(':')) { // 今天的消息,格式为"小时:分钟" const [hour, minute] = time.split(':').map(Number) return today + hour * 3600000 + minute * 60000 } else if (time === '昨天') { // 昨天的消息 return today - 86400000 } else if (time === '周一' || time === '周二' || time === '周三' || time === '周四' || time === '周五' || time === '周六' || time === '周日') { // 本周的消息 const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] const dayIndex = weekdays.indexOf(time) const currentDay = now.getDay() const diff = currentDay - dayIndex return today - diff * 86400000 } else { // 更早的消息 return 0 } }

在这段代码中,我们定义了两个方法:sortContactsconvertTimeToComparablesortContacts方法根据最后消息时间对联系人进行排序,convertTimeToComparable方法将时间字符串转换为可比较的数字格式。在接收新消息或发送消息后,我们可以调用sortContacts方法重新排序联系人列表。

3. 搜索功能

我们可以添加搜索功能,允许用户搜索联系人和消息。

// 状态变量 @State searchText: string = '' @State searchResults: Contact[] = [] @State isSearching: boolean = false // 搜索框 TextInput({ text: this.searchText, placeholder: '搜索联系人' }) .width('90%') .height(40) .margin(10) .backgroundColor('#f5f5f5') .borderRadius(20) .onChange((value: string) => { this.searchText = value this.searchContacts() }) // 搜索联系人方法 private searchContacts() { if (this.searchText.trim() === '') { this.isSearching = false return } this.isSearching = true // 根据搜索文本过滤联系人 this.searchResults = this.contacts.filter(contact => { return contact.name.toLowerCase().includes(this.searchText.toLowerCase()) }) } // 联系人列表渲染 List() { ForEach(this.isSearching ? this.searchResults : this.contacts, (contact: Contact) => { // 联系人项渲染(省略) }) }

在这段代码中,我们添加了三个状态变量:searchTextsearchResultsisSearching。当用户在搜索框中输入文本时,我们调用searchContacts方法根据搜索文本过滤联系人,并将结果存储在searchResults中。在联系人列表渲染中,我们根据isSearching状态决定显示所有联系人还是搜索结果。

优化策略

1. 性能优化

在处理大量消息和联系人时,我们需要注意性能优化,避免不必要的重新渲染和计算。

// 使用懒加载加载更多消息 private loadMoreMessages() { // 模拟加载更多历史消息 const oldestMessageId = this.messages.length > 0 ? this.messages[0].id : 0 // 加载10条历史消息 const historyMessages: Message[] = [] for (let i = 1; i <= 10; i++) { historyMessages.push({ id: oldestMessageId - i, content: `历史消息 ${oldestMessageId - i}`, time: '更早', isMe: i % 2 === 0 }) } // 将历史消息添加到消息列表的前面 this.messages = [...historyMessages.reverse(), ...this.messages] // 重新分组消息 this.groupMessages(this.messages) } // 在消息列表顶部添加加载更多按钮 List() { ListItem() { Button('加载更多') .onClick(() => { this.loadMoreMessages() }) } .justifyContent(FlexAlign.Center) // 消息分组和消息项渲染(省略) }

在这段代码中,我们添加了loadMoreMessages方法用于加载更多历史消息,并在消息列表顶部添加了一个"加载更多"按钮。当用户点击按钮时,我们模拟加载10条历史消息,并将它们添加到消息列表的前面,然后重新分组消息。

2. 用户体验优化

我们可以添加一些动画和过渡效果,提升用户体验。

// 消息项动画 ListItem() { if (message.isMe) { // 自己发送的消息 Row() { // 消息内容(省略) } .justifyContent(FlexAlign.End) .margin({ top: 10, bottom: 10 }) .opacity(message.status === 'sending' ? 0.7 : 1) // 发送中的消息透明度降低 .animation({ // 添加动画效果 duration: 300, curve: Curve.EaseOut }) } else { // 对方发送的消息 Row() { // 消息内容(省略) } .margin({ top: 10, bottom: 10 }) .animation({ // 添加动画效果 duration: 300, curve: Curve.EaseOut }) } }

在这段代码中,我们为消息项添加了动画效果,使消息的显示更加平滑。对于正在发送的消息,我们降低了透明度,使用户能够直观地看到消息的发送状态。

3. 错误处理

在实际应用中,我们需要处理各种错误情况,如消息发送失败、网络连接中断等。

// 消息发送失败处理 private handleSendFailure(messageId: number) { // 查找消息 const index = this.messages.findIndex(msg => msg.id === messageId) if (index !== -1) { // 更新消息状态为发送失败 this.messages[index] = { ...this.messages[index], status: 'failed' } } } // 重试发送消息 private retrySendMessage(messageId: number) { // 查找消息 const index = this.messages.findIndex(msg => msg.id === messageId) if (index !== -1) { // 更新消息状态为发送中 this.messages[index] = { ...this.messages[index], status: 'sending' } // 模拟重新发送 setTimeout(() => { // 随机决定是否发送成功 const isSuccess = Math.random() > 0.3 if (isSuccess) { // 发送成功 this.messages[index] = { ...this.messages[index], status: 'sent' } // 模拟对方已读和回复 setTimeout(() => { this.messages[index] = { ...this.messages[index], status: 'read' } this.simulateReply() }, 2000) } else { // 发送失败 this.handleSendFailure(messageId) } }, 1000) } } // 消息项渲染中添加重试按钮 if (message.status === 'failed') { Row() { Text('发送失败') .fontSize(12) .fontColor('#ff0000') Button('重试') .fontSize(12) .fontColor('#ffffff') .backgroundColor('#ff0000') .borderRadius(10) .padding({ left: 5, right: 5, top: 2, bottom: 2 }) .margin({ left: 5 }) .onClick(() => { this.retrySendMessage(message.id) }) } .margin({ top: 5 }) .alignSelf(ItemAlign.End) }

在这段代码中,我们添加了两个方法:handleSendFailureretrySendMessagehandleSendFailure方法用于处理消息发送失败的情况,将消息状态更新为发送失败。retrySendMessage方法用于重试发送失败的消息,将消息状态更新为发送中,然后模拟重新发送过程。在消息项渲染中,我们为发送失败的消息添加了重试按钮,点击时调用retrySendMessage方法。

小结

在本教程中,我们详细讲解了如何为聊天应用添加交互功能和状态管理,包括联系人切换、消息发送、未读消息处理、消息状态显示、表情选择等功能。我们还介绍了一些高级状态管理技巧,如消息分组显示、联系人排序和搜索功能,以及一些优化策略,如性能优化、用户体验优化和错误处理。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论