HarmonyOS 6商城开发学习:未读消息清除的优雅之道——基于@ObservedV2和@Trace的两种实现
熟悉购物比价应用的朋友一定知道,这类应用的消息通知量非常大:促销活动推送、物流状态更新、商家回复、系统公告……一天下来,右上角的红点数字轻松突破99+。用户想要清理这些未读消息,传统做法通常是提供一个“全部标记已读”按钮,一键清零。但这个方案有个致命缺陷——用户可能只想清除某些不重要的消息,而保留重要的订单通知。全部清除会误伤,逐条点击又太累。
我们之前做过一版消息列表,用的是普通的@State + 数组操作,每次清除都要遍历整个列表,UI重绘量大,而且清除动画生硬。后来我们重构了消息管理模块,基于HarmonyOS最新的@ObservedV2和@Trace装饰器,实现了两种清除未读消息的方式:单项清除和批量选择性清除。这篇文章完整记录一下实现过程和踩坑经验。
功能设计
先说说预期效果。
用户在消息列表页面,可以看到每条消息左侧有一个复选框(多选模式)或右滑出现的“清除”按钮(单项模式)。用户可以选择一条或多条消息,点击“清除未读”按钮,被选中的消息从未读列表中移除,同时列表的未读计数实时更新。整个操作过程要有平滑的动画反馈,不能出现闪烁或卡顿。
核心目标:
-
单项清除:左滑某条消息,出现“清除”按钮,点击后该消息从未读列表消失,未读数减1。
-
批量选择性清除:进入多选模式,勾选多条消息,点击“清除所选”,所有选中消息同时移除,未读数批量减少。
-
性能要求:即使列表中有上千条消息,清除操作也不能卡住UI线程。
核心API
|
API/装饰器 |
说明 |
|---|---|
|
|
类装饰器,使类的属性变化能被观察,支持嵌套对象追踪 |
|
|
属性装饰器,标记需要追踪的属性,配合@ObservedV2使用 |
|
|
列表项的滑动操作事件 |
|
|
显式动画接口,用于清除时的过渡效果 |
实现过程
数据模型改造
首先,我们需要一个可观察的消息模型。传统方式用@State装饰数组,但数组元素内部的属性变化无法被追踪。改用@ObservedV2和@Trace后,每个消息对象的isRead、isSelected等属性变化都能精确触发UI更新。
// model/MessageModel.ets
@ObservedV2
export class MessageItem {
@Trace id: string = '';
@Trace title: string = '';
@Trace content: string = '';
@Trace isRead: boolean = false;
@Trace isSelected: boolean = false; // 多选模式用
@Trace timestamp: number = 0;
}
// 消息列表管理类
@ObservedV2
export class MessageListModel {
@Trace messages: MessageItem[] = [];
get unreadCount(): number {
return this.messages.filter(msg => !msg.isRead).length;
}
// 单项清除
clearSingle(messageId: string): void {
const index = this.messages.findIndex(msg => msg.id === messageId);
if (index !== -1) {
this.messages.splice(index, 1);
}
}
// 批量清除选中的未读消息
clearSelected(): void {
this.messages = this.messages.filter(msg => !msg.isSelected);
}
}
关键点:@ObservedV2装饰类,@Trace装饰每个需要追踪的属性。这样当messages数组的某个元素被删除时,UI只会更新受影响的部分,而不是整个列表重绘。
单项清除:左滑操作
左滑清除是购物比价应用中消息列表的标配交互。我们利用List组件的onSwipeToAction事件实现。
// view/MessageList.ets
@Component
export struct MessageList {
@State messageModel: MessageListModel = new MessageListModel();
build() {
List() {
ForEach(this.messageModel.messages, (item: MessageItem) => {
ListItem() {
MessageCell({ item: item })
}
.swipeAction({
start: this.buildSwipeDeleteButton(item) // 右滑出现删除按钮
})
})
}
}
@Builder
buildSwipeDeleteButton(item: MessageItem) {
Button('清除')
.backgroundColor('#FF3B30')
.width(80)
.height('100%')
.onClick(() => {
animateTo({ duration: 200 }, () => {
this.messageModel.clearSingle(item.id);
});
})
}
}
这里有个细节:清除操作包裹在animateTo中,可以让列表项消失时有淡出或缩放动画,避免突兀消失。
批量选择性清除:多选模式
批量清除需要进入多选状态,用户勾选若干消息后点击“清除所选”。我们用一个@State isMultiSelect来控制是否显示复选框。
// view/MessageList.ets(续)
@State isMultiSelect: boolean = false;
build() {
Column() {
// 顶部操作栏
if (this.isMultiSelect) {
Row() {
Button('取消').onClick(() => this.exitMultiSelect());
Text(`已选 ${this.selectedCount} 条`);
Button('清除所选').onClick(() => this.clearSelectedMessages());
}
}
List() {
ForEach(this.messageModel.messages, (item: MessageItem) => {
ListItem() {
if (this.isMultiSelect) {
Checkbox()
.select(item.isSelected)
.onChange((val) => {
item.isSelected = val;
})
}
MessageCell({ item: item })
}
.onClick(() => {
if (this.isMultiSelect) {
item.isSelected = !item.isSelected;
} else {
// 进入消息详情
}
})
})
}
}
}
clearSelectedMessages() {
animateTo({ duration: 300 }, () => {
this.messageModel.clearSelected();
});
this.exitMultiSelect();
}
注意:这里直接修改item.isSelected,由于@Trace的存在,UI会自动响应,无需手动刷新列表。
遇到的问题与解决方案
问题1:清除后列表闪烁
最初我们在clearSingle中使用splice删除元素,但发现列表会短暂闪烁。原因是splice会触发整个数组的重新排序,而ForEach的key没有正确指定。解决方案:给ForEach提供稳定的key生成器,使用消息的id。
ForEach(this.messageModel.messages,
(item: MessageItem) => { /* ... */ },
(item: MessageItem) => item.id // 关键:用id作为key
)
问题2:批量清除时动画卡顿
当一次性清除上百条消息时,animateTo内部的数组过滤操作会导致大量UI更新堆积。我们采用了分帧处理:将清除操作拆分成小批次,每帧处理一部分,避免主线程阻塞。
async clearSelectedInBatches() {
const toRemove = this.messageModel.messages.filter(msg => msg.isSelected);
const batchSize = 20;
for (let i = 0; i < toRemove.length; i += batchSize) {
const batch = toRemove.slice(i, i + batchSize);
batch.forEach(msg => {
const idx = this.messageModel.messages.indexOf(msg);
if (idx !== -1) {
this.messageModel.messages.splice(idx, 1);
}
});
await new Promise(resolve => setTimeout(resolve, 16)); // 每帧约16ms
}
}
虽然代码看起来有点“土”,但在实际测试中,即使清除500条消息,用户也感觉不到卡顿。
问题3:未读计数更新不及时
未读计数是通过计算属性unreadCount得到的,但一开始我们没有标记它为@Computed,导致UI不会自动刷新。解决方案:使用@Computed装饰计算属性。
@ObservedV2
export class MessageListModel {
@Trace messages: MessageItem[] = [];
@Computed
get unreadCount(): number {
return this.messages.filter(msg => !msg.isRead).length;
}
}
这样每当messages数组或其元素的isRead变化时,unreadCount会自动重新计算并通知UI。
总结
未读消息清除看起来是个小功能,但要做到体验流畅、性能优异,需要合理运用HarmonyOS的响应式编程能力。核心要点总结如下:
|
要点 |
实现方式 |
|---|---|
|
数据模型 |
|
|
单项清除 |
|
|
批量清除 |
多选模式 + |
|
性能优化 |
稳定key、分帧清除、 |
改完之后,消息清除的体验提升了一个档次。用户左滑即可清除单条,长按进入多选模式后可以批量清理,整个过程丝般顺滑,未读数实时更新。如果你也在开发购物比价类应用的消息模块,不妨试试这套方案。
更多推荐



所有评论(0)